diff --git a/package.json b/package.json index 1035048..ab9f1eb 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "@mdi/react": "^1.4.0", "@mdi/svg": "^5.2.45", "@mdi/util": "^0.3.2", - "@sentry/browser": "^5.15.5", - "@sentry/integrations": "^5.15.5", + "@sentry/apm": "^5.17.0", + "@sentry/browser": "^5.17.0", + "@sentry/integrations": "^5.17.0", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^10.0.4", "@testing-library/user-event": "^10.3.1", @@ -63,5 +64,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@sentry/types": "^5.17.0" } } diff --git a/src/components/PanelKit.tsx b/src/components/PanelKit.tsx index ce436df..5f7a09a 100644 --- a/src/components/PanelKit.tsx +++ b/src/components/PanelKit.tsx @@ -3,7 +3,6 @@ import styled from "styled-components"; import { ToastContainer, toast } from "react-toastify"; import { Flex, Box } from "reflexbox/styled-components"; import * as Sentry from "@sentry/browser"; -import { CaptureConsole as CaptureConsoleIntegration } from "@sentry/integrations"; import "react-toastify/dist/ReactToastify.css"; import "../Toast.css"; @@ -13,6 +12,7 @@ import HomeAssistant from "../hass"; import EventManager from "./EventManager"; import Header from "./Header"; import TileErrorBoundary from "./TileErrorBoundary"; +import { setupSentry } from "../sentry"; const Container = styled.div``; @@ -44,14 +44,13 @@ export default class PanelKit extends Component { const sentryDsn = process.env.REACT_APP_SENTRY_DSN || this.props.config.sentryDsn; if (sentryDsn) { - console.log(`[sentry] Initialized with DSN: ${sentryDsn}`); - Sentry.init({ + setupSentry({ dsn: sentryDsn, - integrations: [ - new CaptureConsoleIntegration({ - levels: ["warn", "error"], - }), - ], + release: process.env.REACT_APP_GIT_SHA, + environment: process.env.NODE_ENV || "development", + }); + Sentry.configureScope((scope) => { + scope.setTransaction("panelkit.boot"); }); } diff --git a/src/hass.tsx b/src/hass.tsx index 73634cb..009f7f4 100644 --- a/src/hass.tsx +++ b/src/hass.tsx @@ -1,4 +1,6 @@ import { Entity } from "./types"; +import { startSpan } from "./sentry"; +import { Span, SpanStatus } from "@sentry/apm"; enum State { DISCONNECTED = "DISCONNECTED", @@ -13,6 +15,19 @@ enum Phase { const DEFAULT_TIMEOUT = 250; // ms +const ERROR_MAP: { + [code: number]: SpanStatus; +} = { + // default + 0: SpanStatus.UnknownError, + // A non-increasing identifier has been supplied. + 1: SpanStatus.InvalidArgument, + // Received message is not in expected format (voluptuous validation error). + 2: SpanStatus.InvalidArgument, + // Requested item cannot be found + 3: SpanStatus.NotFound, +}; + const makeSubscriberId = (): string => { const s4 = () => { return Math.floor((1 + Math.random()) * 0x10000) @@ -59,6 +74,7 @@ export interface MessageResult { result: any; error: { message: string; + code?: number; } | null; success: boolean; [key: string]: any; @@ -89,7 +105,7 @@ export default class HomeAssistant { private _messageCounter: number = 1; private _hasPrepared: boolean = false; private _shouldReconnect: boolean = false; - private _pendingRequests: Map; + private _pendingRequests: Map; private _pendingChanges: Map; private _eventSubscribers: EventSubscriber[]; private _rootSubscriptions: Map; @@ -209,10 +225,17 @@ export default class HomeAssistant { switch (payload.type) { case "auth_required": if (this.accessToken) { + // auth requires _no_ ID, which means we can't correlate the message directly + const span = startSpan({ + op: "sendCommand", + description: "auth", + }); this.sendMessage({ type: "auth", access_token: this.accessToken, }); + // TODO(dcramer): this should finish only after auth response + if (span) span.finish(); } else { console.error("[hass] No authentication token configured"); this.disconnect(); @@ -239,14 +262,22 @@ export default class HomeAssistant { if (!promiseHandler) { console.warn("[hass] No pending request found for event", payload.id); } else { - let [resolve, reject] = promiseHandler; + let [resolve, reject, span] = promiseHandler; this._pendingRequests.delete(payload.id); if (payload.success) { + if (span) { + span.setStatus(SpanStatus.Ok); + } setTimeout(() => { this._pendingChanges.delete(payload.id); }, 1000); resolve(payload); } else if (payload.error) { + if (span) { + span.setStatus( + ERROR_MAP[payload.error.code || 0] || SpanStatus.UnknownError + ); + } const error = new Error(payload.error.message); (error as any).payload = payload; const changes = this._pendingChanges.get(payload.id); @@ -258,6 +289,10 @@ export default class HomeAssistant { } reject(error); } + + if (span) { + span.finish(); + } } break; default: @@ -429,8 +464,12 @@ export default class HomeAssistant { suggestedChanges: SuggestedChanges | null = null ): CancellablePromise { const id = this._messageCounter; + const span = startSpan({ + op: "sendCommand", + description: message.type, + }); const promise = new Promise((resolve, reject) => { - this._pendingRequests.set(id, [resolve, reject]); + this._pendingRequests.set(id, [resolve, reject, span]); if (suggestedChanges) { this._pendingChanges.set(id, suggestedChanges); // Object.keys(suggestedChanges).forEach((entityId) => { diff --git a/src/sentry.tsx b/src/sentry.tsx new file mode 100644 index 0000000..bffef62 --- /dev/null +++ b/src/sentry.tsx @@ -0,0 +1,54 @@ +import * as Sentry from "@sentry/browser"; +import { + Integrations as ApmIntegrations, + Span, + Transaction, +} from "@sentry/apm"; +import { CaptureConsole as CaptureConsoleIntegration } from "@sentry/integrations"; + +const Tracing = ApmIntegrations.Tracing; + +export const setupSentry = (options: { [key: string]: any }) => { + console.log(`[sentry] Initialized with DSN: ${options.dsn}`); + Sentry.init({ + tracesSampleRate: 1.0, + ...options, + integrations: [ + new CaptureConsoleIntegration({ + levels: ["error"], + }), + new ApmIntegrations.Tracing(), + ], + }); +}; + +export const startSpan = (options: { + description?: string; + op?: string; +}): Span | undefined => { + const tracingIntegration = Sentry.getCurrentHub().getIntegration(Tracing); + if (!tracingIntegration) { + console.warn("startSpan called without tracing integration"); + return undefined; + } + const transaction = (tracingIntegration as any).constructor.getTransaction(); + if (!transaction) { + console.info("startSpan called without transaction"); + return undefined; + } + return transaction.startChild(options); +}; + +export const startTransaction = (options: { + name: string; + op?: string; + description?: string; +}): Transaction | undefined => { + const hub = Sentry.getCurrentHub(); + const tracingIntegration = hub.getIntegration(Tracing); + if (!tracingIntegration) { + console.warn("startTransaction called without tracing integration"); + return undefined; + } + return Tracing.startIdleTransaction(options); +}; diff --git a/src/tiles/AlarmTile.tsx b/src/tiles/AlarmTile.tsx index 3d804ea..8f3e894 100644 --- a/src/tiles/AlarmTile.tsx +++ b/src/tiles/AlarmTile.tsx @@ -10,10 +10,10 @@ type AlarmTileProps = TileProps & { }; export default class AlarmTile extends Tile { - onTouch = () => { + onTouch = async () => { const { state } = this.getEntity(this.props.entityId); if (ARMED_STATES.has(state)) { - this.callService( + await this.callService( "alarm_control_panel", "alarm_disarm", { diff --git a/src/tiles/AutomationTile.tsx b/src/tiles/AutomationTile.tsx index b399d6e..35cce78 100644 --- a/src/tiles/AutomationTile.tsx +++ b/src/tiles/AutomationTile.tsx @@ -14,8 +14,8 @@ export default class AutomationTile extends Tile { static defaultIcon = "home-automation"; - onTouch = () => { - this.callService("automation", this.props.action, { + onTouch = async () => { + await this.callService("automation", this.props.action, { entity_id: this.props.entityId, }); }; diff --git a/src/tiles/CameraTile.tsx b/src/tiles/CameraTile.tsx index efedd07..c40c081 100644 --- a/src/tiles/CameraTile.tsx +++ b/src/tiles/CameraTile.tsx @@ -64,7 +64,7 @@ export default class CameraTile extends Tile { }); }; - onTouch = () => { + onTouch = async () => { this.openModal(); }; diff --git a/src/tiles/DoorControlTile.tsx b/src/tiles/DoorControlTile.tsx index f562eca..414ab1c 100644 --- a/src/tiles/DoorControlTile.tsx +++ b/src/tiles/DoorControlTile.tsx @@ -11,7 +11,7 @@ type DoorControlTileProps = TileProps & { export default class DoorControlTile extends Tile { static defaultIcon = "door"; - onTouch = () => { + onTouch = async () => { this.openModal(); }; diff --git a/src/tiles/FanTile.tsx b/src/tiles/FanTile.tsx index e7bb8c6..cb4b224 100644 --- a/src/tiles/FanTile.tsx +++ b/src/tiles/FanTile.tsx @@ -5,9 +5,9 @@ type FanTileProps = TileProps & { }; export default class FanTile extends Tile { - onTouch = () => { + onTouch = async () => { const { state } = this.getEntity(this.props.entityId); - this.callService( + await this.callService( "fan", state === "on" ? "turn_off" : "turn_on", { diff --git a/src/tiles/InputSelectTile.tsx b/src/tiles/InputSelectTile.tsx index 2160344..f30478e 100644 --- a/src/tiles/InputSelectTile.tsx +++ b/src/tiles/InputSelectTile.tsx @@ -5,7 +5,7 @@ type InputSelectTileProps = TileProps & { }; export default class InputSelectTile extends Tile { - onTouch = () => { + onTouch = async () => { const { state, attributes: { options }, diff --git a/src/tiles/LightTile.tsx b/src/tiles/LightTile.tsx index ebb5149..f9708ad 100644 --- a/src/tiles/LightTile.tsx +++ b/src/tiles/LightTile.tsx @@ -8,9 +8,9 @@ type LightTileProps = TileProps & { }; export default class LightTile extends Tile { - onTouch = () => { + onTouch = async () => { const { state } = this.getEntity(this.props.entityId); - this.callService( + await this.callService( "light", state === "on" ? "turn_off" : "turn_on", { @@ -22,7 +22,7 @@ export default class LightTile extends Tile { ); }; - onLongTouch = () => { + onLongTouch = async () => { this.openModal(); }; diff --git a/src/tiles/LockTile.tsx b/src/tiles/LockTile.tsx index a29c561..2e76c25 100644 --- a/src/tiles/LockTile.tsx +++ b/src/tiles/LockTile.tsx @@ -5,9 +5,9 @@ type LockTileProps = TileProps & { }; export default class LockTile extends Tile { - onTouch = () => { + onTouch = async () => { const { state } = this.getEntity(this.props.entityId); - this.callService( + await this.callService( "lock", state === "locked" ? "unlock" : "lock", { diff --git a/src/tiles/SceneTile.tsx b/src/tiles/SceneTile.tsx index 8db3546..65db89a 100644 --- a/src/tiles/SceneTile.tsx +++ b/src/tiles/SceneTile.tsx @@ -5,8 +5,8 @@ type SceneTileProps = TileProps & { }; export default class SceneTile extends Tile { - onTouch = () => { - this.callService("scene", "turn_on", { + onTouch = async () => { + await this.callService("scene", "turn_on", { entity_id: this.props.entityId, }); }; diff --git a/src/tiles/ScriptTile.tsx b/src/tiles/ScriptTile.tsx index 0d9bfcc..6880b63 100644 --- a/src/tiles/ScriptTile.tsx +++ b/src/tiles/ScriptTile.tsx @@ -8,9 +8,9 @@ type ScriptTileProps = TileProps & { export default class ScriptTile extends Tile { static defaultIcon = "script"; - onTouch = () => { + onTouch = async () => { let [domain, service] = this.props.entityId.split(".", 2); - this.callService(domain, service, this.props.data); + await this.callService(domain, service, this.props.data); }; renderTitle() { diff --git a/src/tiles/SwitchTile.tsx b/src/tiles/SwitchTile.tsx index 12ec2cd..0fa2095 100644 --- a/src/tiles/SwitchTile.tsx +++ b/src/tiles/SwitchTile.tsx @@ -17,9 +17,9 @@ export default class SwitchTile extends Tile { } } - onTouch = () => { + onTouch = async () => { const { state } = this.getEntity(this.props.entityId); - this.callService( + await this.callService( "switch", state === "on" ? "turn_off" : "turn_on", { diff --git a/src/tiles/Tile.tsx b/src/tiles/Tile.tsx index 7cbdc6c..18bc115 100644 --- a/src/tiles/Tile.tsx +++ b/src/tiles/Tile.tsx @@ -1,9 +1,11 @@ import React, { Component } from "react"; import { toast } from "react-toastify"; +import * as Sentry from "@sentry/browser"; import "./Tile.css"; import LoadingIndicator from "../components/LoadingIndicator"; import Icon from "../components/Icon"; +import { startTransaction } from "../sentry"; import { toTitleCase } from "../utils"; import HomeAssistant from "../hass"; @@ -82,7 +84,7 @@ export default class Tile< if (this._loadingTimer) clearTimeout(this._loadingTimer); } - handleMouseDown = (e: React.MouseEvent) => { + handleMouseDown = async (e: React.MouseEvent) => { if ((e.button === 0 && e.ctrlKey) || e.button > 0) { console.debug("[panelkit] Ignoring click due to mouse button(s)"); return; @@ -90,12 +92,12 @@ export default class Tile< return this.handleButtonPress(e); }; - handleTouchStart = (e: React.TouchEvent) => { + handleTouchStart = async (e: React.TouchEvent) => { if (e.touches.length > 1) return; return this.handleButtonPress(e); }; - handleTouchMove = (e: any) => { + handleTouchMove = async (e: any) => { // We handle the touch move event to avoid a "long press" event from being // fired after someone touchhes and attempts to scroll if (this._longPressTimer) { @@ -105,16 +107,16 @@ export default class Tile< this._wasScrolling = true; }; - handleButtonPress = (e: any) => { + handleButtonPress = async (e: any) => { if (this.onLongTouch) { - this._longPressTimer = setTimeout(() => { - this.onLongTouch && this.onLongTouch(); + this._longPressTimer = setTimeout(async () => { + this.handleOnLongTouch(); this._longPressTimer = null; }, LONG_PRESS_TIME); } }; - handleButtonRelease = ( + handleButtonRelease = async ( e: React.TouchEvent | React.MouseEvent ) => { e.cancelable && e.preventDefault(); @@ -126,13 +128,13 @@ export default class Tile< if (this.onLongTouch && this._longPressTimer) { clearTimeout(this._longPressTimer); this._longPressTimer = null; - this.onTouch && this.onTouch(); + this.handleOnTouch(); } else if (!this.onLongTouch && this.onTouch) { - this.onTouch(); + this.handleOnTouch(); } }; - handleButtonLeave = ( + handleButtonLeave = async ( e: React.TouchEvent | React.MouseEvent ) => { if (this._longPressTimer) { @@ -141,6 +143,34 @@ export default class Tile< } }; + handleOnTouch = async () => { + if (!this.onTouch) return; + Sentry.withScope(async (scope) => { + scope.setTransaction(`panelkit.touch ${this.constructor.name}`); + const transaction = startTransaction({ + name: `panelkit.touch ${this.constructor.name}`, + op: "tile.touch", + description: this.constructor.name, + }); + this.onTouch && (await this.onTouch()); + if (transaction) transaction.finish(); + }); + }; + + handleOnLongTouch = async () => { + if (!this.onLongTouch) return; + Sentry.withScope(async (scope) => { + scope.setTransaction(`panelkit.long-touch ${this.constructor.name}`); + const transaction = startTransaction({ + name: `panelkit.long-touch ${this.constructor.name}`, + op: "tile.long-touch", + description: this.constructor.name, + }); + this.onLongTouch && (await this.onLongTouch()); + if (transaction) transaction.finish(); + }); + }; + onTouch: (() => void) | null = null; onLongTouch: (() => void) | null = null; @@ -160,7 +190,7 @@ export default class Tile< return this.props.hass.getEntity(entityId); } - callService( + async callService( domain: string, service: string, serviceData: any, diff --git a/yarn.lock b/yarn.lock index fc7a473..23bd81d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1374,65 +1374,77 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@sentry/browser@^5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36" - integrity sha512-rqDvjk/EvogfdbZ4TiEpxM/lwpPKmq23z9YKEO4q81+1SwJNua53H60dOk9HpRU8nOJ1g84TMKT2Ov8H7sqDWA== - dependencies: - "@sentry/core" "5.15.5" - "@sentry/types" "5.15.5" - "@sentry/utils" "5.15.5" +"@sentry/apm@^5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.17.0.tgz#c3e6d07f4f488f77c8677cdc8d0e2fb58e302d72" + integrity sha512-raJcPa04TP8mVocSTHe0PdULpRWhw0NaLq9Rk8KCTFBJvLsgzY2krph5/LgEfBBX78vWt70FrwSw+DdIfYIJ6g== + dependencies: + "@sentry/browser" "5.17.0" + "@sentry/hub" "5.17.0" + "@sentry/minimal" "5.17.0" + "@sentry/types" "5.17.0" + "@sentry/utils" "5.17.0" tslib "^1.9.3" -"@sentry/core@5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.5.tgz#40ea79bff5272d3fbbeeb4a98cdc59e1adbd2c92" - integrity sha512-enxBLv5eibBMqcWyr+vApqeix8uqkfn0iGsD3piKvoMXCgKsrfMwlb/qo9Ox0lKr71qIlZVt+9/A2vZohdgnlg== +"@sentry/browser@5.17.0", "@sentry/browser@^5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.17.0.tgz#0c3796cb02df3ec8db13341564fae0bc83e148c5" + integrity sha512-++pXpCHtdek1cRUwVeLvlxUJ2w1s+eiC9qN1N+7+HdAjHpBz2/tA1sKBCqwwVQZ490Cf2GLll9Ao7fuPPmveRQ== dependencies: - "@sentry/hub" "5.15.5" - "@sentry/minimal" "5.15.5" - "@sentry/types" "5.15.5" - "@sentry/utils" "5.15.5" + "@sentry/core" "5.17.0" + "@sentry/types" "5.17.0" + "@sentry/utils" "5.17.0" tslib "^1.9.3" -"@sentry/hub@5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.5.tgz#f5abbcdbe656a70e2ff02c02a5a4cffa0f125935" - integrity sha512-zX9o49PcNIVMA4BZHe//GkbQ4Jx+nVofqU/Il32/IbwKhcpPlhGX3c1sOVQo4uag3cqd/JuQsk+DML9TKkN0Lw== +"@sentry/core@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.17.0.tgz#b2deef95465c766076d5cffd8534a67100f9b821" + integrity sha512-Kfx4rGKDC7V1YJjTGJXyl12VVHxM8Cjpu61YOyF8kXoXXg9u06C3n0G1dmfzLQERKXasUVMtXRBdKx/OjYpl1g== dependencies: - "@sentry/types" "5.15.5" - "@sentry/utils" "5.15.5" + "@sentry/hub" "5.17.0" + "@sentry/minimal" "5.17.0" + "@sentry/types" "5.17.0" + "@sentry/utils" "5.17.0" tslib "^1.9.3" -"@sentry/integrations@^5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-5.15.5.tgz#7f7bc488d838cd50e9ca50d5f933680632827826" - integrity sha512-s9N9altnGkDH+vNNUZu1dKuMVLAgJNYtgs6DMJTrZRswFl8gzZytYTZCdpzjBgTsqkLaGbRDIjQeE/yP3gnrqw== +"@sentry/hub@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.17.0.tgz#b7d255ca3f766385911d9414af97f388e869d996" + integrity sha512-lyUbEmshwaMYdAzy4iwgizgvKODVVloB2trnefpq90AuWCdvzcxMLIGULx1ou+KohccqdNorYICKWeuRscKq5A== dependencies: - "@sentry/types" "5.15.5" - "@sentry/utils" "5.15.5" + "@sentry/types" "5.17.0" + "@sentry/utils" "5.17.0" tslib "^1.9.3" -"@sentry/minimal@5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.5.tgz#a0e4e071f01d9c4d808094ae7203f6c4cca9348a" - integrity sha512-zQkkJ1l9AjmU/Us5IrOTzu7bic4sTPKCatptXvLSTfyKW7N6K9MPIIFeSpZf9o1yM2sRYdK7GV08wS2eCT3JYw== +"@sentry/integrations@^5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-5.17.0.tgz#afff7759d82111de030b4a6703388c423fdbe6e7" + integrity sha512-H4CLH+fej/EjbI5WKXnAVkyVK3MeHUcTMbnjPcUlAsxpu1+PckFzpw3t4S5la9WGwcfL3WDo24b+fb4iKf9t4Q== dependencies: - "@sentry/hub" "5.15.5" - "@sentry/types" "5.15.5" + "@sentry/types" "5.17.0" + "@sentry/utils" "5.17.0" tslib "^1.9.3" -"@sentry/types@5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.5.tgz#16c97e464cf09bbd1d2e8ce90d130e781709076e" - integrity sha512-F9A5W7ucgQLJUG4LXw1ZIy4iLevrYZzbeZ7GJ09aMlmXH9PqGThm1t5LSZlVpZvUfQ2rYA8NU6BdKJSt7B5LPw== +"@sentry/minimal@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.17.0.tgz#b40e4b4109b098840277def3b51cc20ae6767164" + integrity sha512-v8xfkySXKrliZO6er6evlVe/ViUcqN0O8BhGyauK28Mf+KnKEOs5W6oWbt4qCDIttw9ynKIYyRrkAl/9oUR76A== + dependencies: + "@sentry/hub" "5.17.0" + "@sentry/types" "5.17.0" + tslib "^1.9.3" + +"@sentry/types@5.17.0", "@sentry/types@^5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.17.0.tgz#b8d245ac7d5caa749c549e9f72aab2d6522afe63" + integrity sha512-1z8EXzvg8GcsBNnSXgB5/G7mz2PwmMt9mjOrVG1jhtSGH1c7WvB32F5boqoMcjIJmy5MrBGaaXwrF/RRJrwUQg== -"@sentry/utils@5.15.5": - version "5.15.5" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.5.tgz#dec1d4c79037c4da08b386f5d34409234dcbfb15" - integrity sha512-Nl9gl/MGnzSkuKeo3QaefoD/OJrFLB8HmwQ7HUbTXb6E7yyEzNKAQMHXGkwNAjbdYyYbd42iABP6Y5F/h39NtA== +"@sentry/utils@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.17.0.tgz#b809b067665f3ebaea77ba7b5d1d1d14a4ed76cb" + integrity sha512-qn8WgZcSkV/rx0ezp9q/xFjP7aMaYZO1/JYLXV4o6pYrQ9tvMmmwAZT39FpJunhhbkR36WNEuRB9C2K250cb/A== dependencies: - "@sentry/types" "5.15.5" + "@sentry/types" "5.17.0" tslib "^1.9.3" "@sinonjs/commons@^1.7.0":