diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a373bbae480..c759af697841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,16 @@ ## Unreleased - - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.53.1 + +- chore(deps): bump socket.io-parser from 4.2.1 to 4.2.3 (#8196) +- chore(svelte): Bump magic-string to 0.30.0 (#8197) +- fix(core): Fix racecondition that modifies in-flight sessions (#8203) +- fix(node): Catch `os.uptime()` throwing because of EPERM (#8206) +- fix(replay): Fix buffered replays creating replay w/o error occuring (#8168) + ## 7.53.0 - feat(replay): Add `beforeAddRecordingEvent` Replay option (#8124) diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 0a4cd6a87870..5e79d1707d67 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -48,7 +48,7 @@ export function createSessionEnvelope( }; const envelopeItem: SessionItem = - 'aggregates' in session ? [{ type: 'sessions' }, session] : [{ type: 'session' }, session]; + 'aggregates' in session ? [{ type: 'sessions' }, session] : [{ type: 'session' }, session.toJSON()]; return createEnvelope(envelopeHeaders, [envelopeItem]); } diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index 33282d992716..3039b5fae58b 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -195,10 +195,17 @@ function getAppContext(): AppContext { export function getDeviceContext(deviceOpt: DeviceContextOptions | true): DeviceContext { const device: DeviceContext = {}; + // Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202 + let uptime; + try { + uptime = os.uptime && os.uptime(); + } catch (e) { + // noop + } + // os.uptime or its return value seem to be undefined in certain environments (e.g. Azure functions). // Hence, we only set boot time, if we get a valid uptime value. // @see https://github.com/getsentry/sentry-javascript/issues/5856 - const uptime = os.uptime && os.uptime(); if (typeof uptime === 'number') { device.boot_time = new Date(Date.now() - uptime * 1000).toISOString(); } diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index b485f3c882d1..665d29e2386a 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -128,6 +128,24 @@ function checkCallForSentReplay( }; } +/** +* Only want calls that send replay events, i.e. ignore error events +*/ +function getReplayCalls(calls: any[][][]): any[][][] { + return calls.map(call => { + const arg = call[0]; + if (arg.length !== 2) { + return []; + } + + if (!arg[1][0].find(({type}: {type: string}) => ['replay_event', 'replay_recording'].includes(type))) { + return []; + } + + return [ arg ]; + }).filter(Boolean); +} + /** * Checks all calls to `fetch` and ensures a replay was uploaded by * checking the `fetch()` request's body. @@ -143,7 +161,9 @@ const toHaveSentReplay = function ( const expectedKeysLength = expected ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length : 0; - for (const currentCall of calls) { + const replayCalls = getReplayCalls(calls) + + for (const currentCall of replayCalls) { result = checkCallForSentReplay.call(this, currentCall[0], expected); if (result.pass) { break; @@ -193,7 +213,9 @@ const toHaveLastSentReplay = function ( expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, ) { const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock; - const lastCall = calls[calls.length - 1]?.[0]; + const replayCalls = getReplayCalls(calls) + + const lastCall = replayCalls[calls.length - 1]?.[0]; const { results, call, pass } = checkCallForSentReplay.call(this, lastCall, expected); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index e45830e9fdde..52c772f57c21 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -327,7 +327,9 @@ export class ReplayContainer implements ReplayContainerInterface { this._debouncedFlush.cancel(); // See comment above re: `_isEnabled`, we "force" a flush, ignoring the // `_isEnabled` state of the plugin since it was disabled above. - await this._flush({ force: true }); + if (this.recordingMode === 'session') { + await this._flush({ force: true }); + } // After flush, destroy event buffer this.eventBuffer && this.eventBuffer.destroy(); diff --git a/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts b/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts index c0b711c028f8..20645b1b85a4 100644 --- a/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts +++ b/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts @@ -229,6 +229,21 @@ describe('Integration | errorSampleRate with delayed flush', () => { }); }); + // This tests a regression where we were calling flush indiscriminantly in `stop()` + it('does not upload a replay event if error is not sampled', async () => { + // We are trying to replicate the case where error rate is 0 and session + // rate is > 0, we can't set them both to 0 otherwise + // `_loadAndCheckSession` is not called when initializing the plugin. + replay.stop(); + replay['_options']['errorSampleRate'] = 0; + replay['_loadAndCheckSession'](); + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + }); + it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { Object.defineProperty(document, 'visibilityState', { configurable: true, @@ -664,7 +679,7 @@ describe('Integration | errorSampleRate with delayed flush', () => { jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); await new Promise(process.nextTick); - expect(replay).toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); // Wait a bit, shortly before session expires jest.advanceTimersByTime(MAX_SESSION_LIFE - 1000); diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index 74fde11f50f0..3145ba37e7f9 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -234,6 +234,21 @@ describe('Integration | errorSampleRate', () => { }); }); + // This tests a regression where we were calling flush indiscriminantly in `stop()` + it('does not upload a replay event if error is not sampled', async () => { + // We are trying to replicate the case where error rate is 0 and session + // rate is > 0, we can't set them both to 0 otherwise + // `_loadAndCheckSession` is not called when initializing the plugin. + replay.stop(); + replay['_options']['errorSampleRate'] = 0; + replay['_loadAndCheckSession'](); + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + }); + it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { Object.defineProperty(document, 'visibilityState', { configurable: true, @@ -668,8 +683,7 @@ describe('Integration | errorSampleRate', () => { jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); // Wait a bit, shortly before session expires jest.advanceTimersByTime(MAX_SESSION_LIFE - 1000); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 51a3330e19c0..ffcd4809938e 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -19,7 +19,7 @@ "@sentry/browser": "7.53.0", "@sentry/types": "7.53.0", "@sentry/utils": "7.53.0", - "magic-string": "^0.26.2", + "magic-string": "^0.30.0", "tslib": "^1.9.3" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 3c2dfa74088b..2adcbff1f3db 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -28,6 +28,7 @@ "@sentry/types": "7.53.0", "@sentry/utils": "7.53.0", "@sentry/vite-plugin": "^0.6.0", + "magicast": "0.2.6", "sorcery": "0.11.0" }, "devDependencies": { diff --git a/packages/sveltekit/src/vite/autoInstrument.ts b/packages/sveltekit/src/vite/autoInstrument.ts index 136d1fef0148..cabfc743db0c 100644 --- a/packages/sveltekit/src/vite/autoInstrument.ts +++ b/packages/sveltekit/src/vite/autoInstrument.ts @@ -1,5 +1,7 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ +import type { ExportNamedDeclaration } from '@babel/types'; import * as fs from 'fs'; +import { parseModule } from 'magicast'; import * as path from 'path'; import type { Plugin } from 'vite'; @@ -89,24 +91,50 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio */ export async function canWrapLoad(id: string, debug: boolean): Promise { const code = (await fs.promises.readFile(id, 'utf8')).toString(); + const mod = parseModule(code); - const codeWithoutComments = code.replace(/(\/\/.*| ?\/\*[^]*?\*\/)(,?)$/gm, ''); - - const hasSentryContent = codeWithoutComments.includes('@sentry/sveltekit'); - if (hasSentryContent) { + const program = mod.$ast.type === 'Program' && mod.$ast; + if (!program) { // eslint-disable-next-line no-console - debug && console.log(`Skipping wrapping ${id} because it already contains Sentry code`); + debug && console.log(`Skipping wrapping ${id} because it doesn't contain valid JavaScript or TypeScript`); + return false; } - const hasLoadDeclaration = /((const|let|var|function)\s+load\s*(=|\(|:))|as\s+load\s*(,|})/gm.test( - codeWithoutComments, - ); + const hasLoadDeclaration = program.body + .filter((statement): statement is ExportNamedDeclaration => statement.type === 'ExportNamedDeclaration') + .find(exportDecl => { + // find `export const load = ...` + if (exportDecl.declaration && exportDecl.declaration.type === 'VariableDeclaration') { + const variableDeclarations = exportDecl.declaration.declarations; + return variableDeclarations.find(decl => decl.id.type === 'Identifier' && decl.id.name === 'load'); + } + + // find `export function load = ...` + if (exportDecl.declaration && exportDecl.declaration.type === 'FunctionDeclaration') { + const functionId = exportDecl.declaration.id; + return functionId?.name === 'load'; + } + + // find `export { load, somethingElse as load, somethingElse as "load" }` + if (exportDecl.specifiers) { + return exportDecl.specifiers.find(specifier => { + return ( + (specifier.exported.type === 'Identifier' && specifier.exported.name === 'load') || + (specifier.exported.type === 'StringLiteral' && specifier.exported.value === 'load') + ); + }); + } + + return false; + }); + if (!hasLoadDeclaration) { // eslint-disable-next-line no-console debug && console.log(`Skipping wrapping ${id} because it doesn't declare a \`load\` function`); + return false; } - return !hasSentryContent && hasLoadDeclaration; + return true; } /** diff --git a/packages/sveltekit/test/vite/autoInstrument.test.ts b/packages/sveltekit/test/vite/autoInstrument.test.ts index 0b5599912e7a..954138f017bf 100644 --- a/packages/sveltekit/test/vite/autoInstrument.test.ts +++ b/packages/sveltekit/test/vite/autoInstrument.test.ts @@ -121,6 +121,7 @@ describe('canWrapLoad', () => { ['export variable declaration - function pointer', 'export const load= loadPageData'], ['export variable declaration - factory function call', 'export const load =loadPageData()'], ['export variable declaration - inline function', 'export const load = () => { return { props: { msg: "hi" } } }'], + ['export variable declaration - inline function let', 'export let load = () => {}'], [ 'export variable declaration - inline async function', 'export const load = async () => { return { props: { msg: "hi" } } }', @@ -139,14 +140,14 @@ describe('canWrapLoad', () => { 'variable declaration (let)', `import {something} from 'somewhere'; let load = async () => {}; - export prerender = true; + export const prerender = true; export { load}`, ], [ 'variable declaration (var)', `import {something} from 'somewhere'; var load=async () => {}; - export prerender = true; + export const prerender = true; export { load}`, ], @@ -176,13 +177,18 @@ describe('canWrapLoad', () => { async function somethingElse(){}; export { somethingElse as load, foo }`, ], - + [ + 'function declaration with different string literal name', + `import { foo } from 'somewhere'; + async function somethingElse(){}; + export { somethingElse as "load", foo }`, + ], [ 'export variable declaration - inline function with assigned type', `import type { LayoutLoad } from './$types'; export const load : LayoutLoad = async () => { return { props: { msg: "hi" } } }`, ], - ])('returns `true` if a load declaration (%s) exists and no Sentry code was found', async (_, code) => { + ])('returns `true` if a load declaration (%s) exists', async (_, code) => { fileContent = code; expect(await canWrapLoad('+page.ts', false)).toEqual(true); }); @@ -203,14 +209,4 @@ describe('canWrapLoad', () => { fileContent = code; expect(await canWrapLoad('+page.ts', false)).toEqual(true); }); - - it('returns `false` if Sentry code was found', async () => { - fileContent = 'import * as Sentry from "@sentry/sveltekit";'; - expect(await canWrapLoad('+page.ts', false)).toEqual(false); - }); - - it('returns `false` if Sentry code was found', async () => { - fileContent = 'import * as Sentry from "@sentry/sveltekit";'; - expect(await canWrapLoad('+page.ts', false)).toEqual(false); - }); }); diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 3bcb8e96da7d..3f3ebf999ef9 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -4,7 +4,7 @@ import type { DsnComponents } from './dsn'; import type { Event } from './event'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; -import type { Session, SessionAggregates } from './session'; +import type { SerializedSession, Session, SessionAggregates } from './session'; import type { Transaction } from './transaction'; import type { UserFeedback } from './user'; @@ -76,7 +76,8 @@ export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; export type UserFeedbackItem = BaseEnvelopeItem; export type SessionItem = - | BaseEnvelopeItem + // TODO(v8): Only allow serialized session here (as opposed to Session or SerializedSesison) + | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; export type CheckInItem = BaseEnvelopeItem; diff --git a/yarn.lock b/yarn.lock index eee0eb3cd55f..4c8836ca0838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -970,6 +970,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -1027,6 +1032,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== +"@babel/parser@^7.21.8": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -2218,6 +2228,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== + dependencies: + "@babel/helper-string-parser" "^7.21.5" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -6565,6 +6584,13 @@ ast-types@0.14.2: dependencies: tslib "^2.0.1" +ast-types@0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" + integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== + dependencies: + tslib "^2.0.1" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -17958,13 +17984,6 @@ magic-string@^0.25.0, magic-string@^0.25.1, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.26.2: - version "0.26.3" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.3.tgz#25840b875140f7b4785ab06bddc384270b7dd452" - integrity sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg== - dependencies: - sourcemap-codec "^1.4.8" - magic-string@^0.29.0: version "0.29.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3" @@ -17979,6 +17998,15 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" +magicast@0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.2.6.tgz#08c9f1900177ca1896e9c07981912171d4ed8ec1" + integrity sha512-6bX0nVjGrA41o+qHSv9Duiv3VuF7jUyjT7dIb3E61YW/5mucvCBMgyZssUznRc+xlUMPYyXZZluZjE1k5z+2yQ== + dependencies: + "@babel/parser" "^7.21.8" + "@babel/types" "^7.21.5" + recast "^0.22.0" + make-dir@3.1.0, make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0, make-dir@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -22909,6 +22937,17 @@ recast@^0.20.5: source-map "~0.6.1" tslib "^2.0.1" +recast@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.22.0.tgz#1dd3bf1b86e5eb810b044221a1a734234ed3e9c0" + integrity sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ== + dependencies: + assert "^2.0.0" + ast-types "0.15.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -24333,9 +24372,9 @@ socket.io-adapter@~2.4.0: integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== socket.io-parser@~4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" - integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== + version "4.2.3" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" + integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1"