Skip to content

Commit 8c0c3f2

Browse files
author
Luca Forstner
authored
Merge pull request #8208 from getsentry/prepare-release/7.53.1
2 parents 5082e83 + 7d2be81 commit 8c0c3f2

File tree

13 files changed

+177
-45
lines changed

13 files changed

+177
-45
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
## Unreleased
44

5-
65
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
76

7+
## 7.53.1
8+
9+
- chore(deps): bump socket.io-parser from 4.2.1 to 4.2.3 (#8196)
10+
- chore(svelte): Bump magic-string to 0.30.0 (#8197)
11+
- fix(core): Fix racecondition that modifies in-flight sessions (#8203)
12+
- fix(node): Catch `os.uptime()` throwing because of EPERM (#8206)
13+
- fix(replay): Fix buffered replays creating replay w/o error occuring (#8168)
14+
815
## 7.53.0
916

1017
- feat(replay): Add `beforeAddRecordingEvent` Replay option (#8124)

packages/core/src/envelope.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function createSessionEnvelope(
4848
};
4949

5050
const envelopeItem: SessionItem =
51-
'aggregates' in session ? [{ type: 'sessions' }, session] : [{ type: 'session' }, session];
51+
'aggregates' in session ? [{ type: 'sessions' }, session] : [{ type: 'session' }, session.toJSON()];
5252

5353
return createEnvelope<SessionEnvelope>(envelopeHeaders, [envelopeItem]);
5454
}

packages/node/src/integrations/context.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,17 @@ function getAppContext(): AppContext {
195195
export function getDeviceContext(deviceOpt: DeviceContextOptions | true): DeviceContext {
196196
const device: DeviceContext = {};
197197

198+
// Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202
199+
let uptime;
200+
try {
201+
uptime = os.uptime && os.uptime();
202+
} catch (e) {
203+
// noop
204+
}
205+
198206
// os.uptime or its return value seem to be undefined in certain environments (e.g. Azure functions).
199207
// Hence, we only set boot time, if we get a valid uptime value.
200208
// @see https://github.com/getsentry/sentry-javascript/issues/5856
201-
const uptime = os.uptime && os.uptime();
202209
if (typeof uptime === 'number') {
203210
device.boot_time = new Date(Date.now() - uptime * 1000).toISOString();
204211
}

packages/replay/jest.setup.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,24 @@ function checkCallForSentReplay(
128128
};
129129
}
130130

131+
/**
132+
* Only want calls that send replay events, i.e. ignore error events
133+
*/
134+
function getReplayCalls(calls: any[][][]): any[][][] {
135+
return calls.map(call => {
136+
const arg = call[0];
137+
if (arg.length !== 2) {
138+
return [];
139+
}
140+
141+
if (!arg[1][0].find(({type}: {type: string}) => ['replay_event', 'replay_recording'].includes(type))) {
142+
return [];
143+
}
144+
145+
return [ arg ];
146+
}).filter(Boolean);
147+
}
148+
131149
/**
132150
* Checks all calls to `fetch` and ensures a replay was uploaded by
133151
* checking the `fetch()` request's body.
@@ -143,7 +161,9 @@ const toHaveSentReplay = function (
143161

144162
const expectedKeysLength = expected ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length : 0;
145163

146-
for (const currentCall of calls) {
164+
const replayCalls = getReplayCalls(calls)
165+
166+
for (const currentCall of replayCalls) {
147167
result = checkCallForSentReplay.call(this, currentCall[0], expected);
148168
if (result.pass) {
149169
break;
@@ -193,7 +213,9 @@ const toHaveLastSentReplay = function (
193213
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
194214
) {
195215
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
196-
const lastCall = calls[calls.length - 1]?.[0];
216+
const replayCalls = getReplayCalls(calls)
217+
218+
const lastCall = replayCalls[calls.length - 1]?.[0];
197219

198220
const { results, call, pass } = checkCallForSentReplay.call(this, lastCall, expected);
199221

packages/replay/src/replay.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,9 @@ export class ReplayContainer implements ReplayContainerInterface {
327327
this._debouncedFlush.cancel();
328328
// See comment above re: `_isEnabled`, we "force" a flush, ignoring the
329329
// `_isEnabled` state of the plugin since it was disabled above.
330-
await this._flush({ force: true });
330+
if (this.recordingMode === 'session') {
331+
await this._flush({ force: true });
332+
}
331333

332334
// After flush, destroy event buffer
333335
this.eventBuffer && this.eventBuffer.destroy();

packages/replay/test/integration/errorSampleRate-delayFlush.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,21 @@ describe('Integration | errorSampleRate with delayed flush', () => {
229229
});
230230
});
231231

232+
// This tests a regression where we were calling flush indiscriminantly in `stop()`
233+
it('does not upload a replay event if error is not sampled', async () => {
234+
// We are trying to replicate the case where error rate is 0 and session
235+
// rate is > 0, we can't set them both to 0 otherwise
236+
// `_loadAndCheckSession` is not called when initializing the plugin.
237+
replay.stop();
238+
replay['_options']['errorSampleRate'] = 0;
239+
replay['_loadAndCheckSession']();
240+
241+
jest.runAllTimers();
242+
await new Promise(process.nextTick);
243+
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
244+
expect(replay).not.toHaveLastSentReplay();
245+
});
246+
232247
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
233248
Object.defineProperty(document, 'visibilityState', {
234249
configurable: true,
@@ -664,7 +679,7 @@ describe('Integration | errorSampleRate with delayed flush', () => {
664679
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
665680
await new Promise(process.nextTick);
666681

667-
expect(replay).toHaveLastSentReplay();
682+
expect(replay).not.toHaveLastSentReplay();
668683

669684
// Wait a bit, shortly before session expires
670685
jest.advanceTimersByTime(MAX_SESSION_LIFE - 1000);

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,21 @@ describe('Integration | errorSampleRate', () => {
234234
});
235235
});
236236

237+
// This tests a regression where we were calling flush indiscriminantly in `stop()`
238+
it('does not upload a replay event if error is not sampled', async () => {
239+
// We are trying to replicate the case where error rate is 0 and session
240+
// rate is > 0, we can't set them both to 0 otherwise
241+
// `_loadAndCheckSession` is not called when initializing the plugin.
242+
replay.stop();
243+
replay['_options']['errorSampleRate'] = 0;
244+
replay['_loadAndCheckSession']();
245+
246+
jest.runAllTimers();
247+
await new Promise(process.nextTick);
248+
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
249+
expect(replay).not.toHaveLastSentReplay();
250+
});
251+
237252
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
238253
Object.defineProperty(document, 'visibilityState', {
239254
configurable: true,
@@ -668,8 +683,7 @@ describe('Integration | errorSampleRate', () => {
668683

669684
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
670685
await new Promise(process.nextTick);
671-
672-
expect(replay).toHaveLastSentReplay();
686+
expect(replay).not.toHaveLastSentReplay();
673687

674688
// Wait a bit, shortly before session expires
675689
jest.advanceTimersByTime(MAX_SESSION_LIFE - 1000);

packages/svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@sentry/browser": "7.53.0",
2020
"@sentry/types": "7.53.0",
2121
"@sentry/utils": "7.53.0",
22-
"magic-string": "^0.26.2",
22+
"magic-string": "^0.30.0",
2323
"tslib": "^1.9.3"
2424
},
2525
"peerDependencies": {

packages/sveltekit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@sentry/types": "7.53.0",
2929
"@sentry/utils": "7.53.0",
3030
"@sentry/vite-plugin": "^0.6.0",
31+
"magicast": "0.2.6",
3132
"sorcery": "0.11.0"
3233
},
3334
"devDependencies": {

packages/sveltekit/src/vite/autoInstrument.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
import type { ExportNamedDeclaration } from '@babel/types';
23
import * as fs from 'fs';
4+
import { parseModule } from 'magicast';
35
import * as path from 'path';
46
import type { Plugin } from 'vite';
57

@@ -89,24 +91,50 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
8991
*/
9092
export async function canWrapLoad(id: string, debug: boolean): Promise<boolean> {
9193
const code = (await fs.promises.readFile(id, 'utf8')).toString();
94+
const mod = parseModule(code);
9295

93-
const codeWithoutComments = code.replace(/(\/\/.*| ?\/\*[^]*?\*\/)(,?)$/gm, '');
94-
95-
const hasSentryContent = codeWithoutComments.includes('@sentry/sveltekit');
96-
if (hasSentryContent) {
96+
const program = mod.$ast.type === 'Program' && mod.$ast;
97+
if (!program) {
9798
// eslint-disable-next-line no-console
98-
debug && console.log(`Skipping wrapping ${id} because it already contains Sentry code`);
99+
debug && console.log(`Skipping wrapping ${id} because it doesn't contain valid JavaScript or TypeScript`);
100+
return false;
99101
}
100102

101-
const hasLoadDeclaration = /((const|let|var|function)\s+load\s*(=|\(|:))|as\s+load\s*(,|})/gm.test(
102-
codeWithoutComments,
103-
);
103+
const hasLoadDeclaration = program.body
104+
.filter((statement): statement is ExportNamedDeclaration => statement.type === 'ExportNamedDeclaration')
105+
.find(exportDecl => {
106+
// find `export const load = ...`
107+
if (exportDecl.declaration && exportDecl.declaration.type === 'VariableDeclaration') {
108+
const variableDeclarations = exportDecl.declaration.declarations;
109+
return variableDeclarations.find(decl => decl.id.type === 'Identifier' && decl.id.name === 'load');
110+
}
111+
112+
// find `export function load = ...`
113+
if (exportDecl.declaration && exportDecl.declaration.type === 'FunctionDeclaration') {
114+
const functionId = exportDecl.declaration.id;
115+
return functionId?.name === 'load';
116+
}
117+
118+
// find `export { load, somethingElse as load, somethingElse as "load" }`
119+
if (exportDecl.specifiers) {
120+
return exportDecl.specifiers.find(specifier => {
121+
return (
122+
(specifier.exported.type === 'Identifier' && specifier.exported.name === 'load') ||
123+
(specifier.exported.type === 'StringLiteral' && specifier.exported.value === 'load')
124+
);
125+
});
126+
}
127+
128+
return false;
129+
});
130+
104131
if (!hasLoadDeclaration) {
105132
// eslint-disable-next-line no-console
106133
debug && console.log(`Skipping wrapping ${id} because it doesn't declare a \`load\` function`);
134+
return false;
107135
}
108136

109-
return !hasSentryContent && hasLoadDeclaration;
137+
return true;
110138
}
111139

112140
/**

packages/sveltekit/test/vite/autoInstrument.test.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ describe('canWrapLoad', () => {
121121
['export variable declaration - function pointer', 'export const load= loadPageData'],
122122
['export variable declaration - factory function call', 'export const load =loadPageData()'],
123123
['export variable declaration - inline function', 'export const load = () => { return { props: { msg: "hi" } } }'],
124+
['export variable declaration - inline function let', 'export let load = () => {}'],
124125
[
125126
'export variable declaration - inline async function',
126127
'export const load = async () => { return { props: { msg: "hi" } } }',
@@ -139,14 +140,14 @@ describe('canWrapLoad', () => {
139140
'variable declaration (let)',
140141
`import {something} from 'somewhere';
141142
let load = async () => {};
142-
export prerender = true;
143+
export const prerender = true;
143144
export { load}`,
144145
],
145146
[
146147
'variable declaration (var)',
147148
`import {something} from 'somewhere';
148149
var load=async () => {};
149-
export prerender = true;
150+
export const prerender = true;
150151
export { load}`,
151152
],
152153

@@ -176,13 +177,18 @@ describe('canWrapLoad', () => {
176177
async function somethingElse(){};
177178
export { somethingElse as load, foo }`,
178179
],
179-
180+
[
181+
'function declaration with different string literal name',
182+
`import { foo } from 'somewhere';
183+
async function somethingElse(){};
184+
export { somethingElse as "load", foo }`,
185+
],
180186
[
181187
'export variable declaration - inline function with assigned type',
182188
`import type { LayoutLoad } from './$types';
183189
export const load : LayoutLoad = async () => { return { props: { msg: "hi" } } }`,
184190
],
185-
])('returns `true` if a load declaration (%s) exists and no Sentry code was found', async (_, code) => {
191+
])('returns `true` if a load declaration (%s) exists', async (_, code) => {
186192
fileContent = code;
187193
expect(await canWrapLoad('+page.ts', false)).toEqual(true);
188194
});
@@ -203,14 +209,4 @@ describe('canWrapLoad', () => {
203209
fileContent = code;
204210
expect(await canWrapLoad('+page.ts', false)).toEqual(true);
205211
});
206-
207-
it('returns `false` if Sentry code was found', async () => {
208-
fileContent = 'import * as Sentry from "@sentry/sveltekit";';
209-
expect(await canWrapLoad('+page.ts', false)).toEqual(false);
210-
});
211-
212-
it('returns `false` if Sentry code was found', async () => {
213-
fileContent = 'import * as Sentry from "@sentry/sveltekit";';
214-
expect(await canWrapLoad('+page.ts', false)).toEqual(false);
215-
});
216212
});

packages/types/src/envelope.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { DsnComponents } from './dsn';
44
import type { Event } from './event';
55
import type { ReplayEvent, ReplayRecordingData } from './replay';
66
import type { SdkInfo } from './sdkinfo';
7-
import type { Session, SessionAggregates } from './session';
7+
import type { SerializedSession, Session, SessionAggregates } from './session';
88
import type { Transaction } from './transaction';
99
import type { UserFeedback } from './user';
1010

@@ -76,7 +76,8 @@ export type EventItem = BaseEnvelopeItem<EventItemHeaders, Event>;
7676
export type AttachmentItem = BaseEnvelopeItem<AttachmentItemHeaders, string | Uint8Array>;
7777
export type UserFeedbackItem = BaseEnvelopeItem<UserFeedbackItemHeaders, UserFeedback>;
7878
export type SessionItem =
79-
| BaseEnvelopeItem<SessionItemHeaders, Session>
79+
// TODO(v8): Only allow serialized session here (as opposed to Session or SerializedSesison)
80+
| BaseEnvelopeItem<SessionItemHeaders, Session | SerializedSession>
8081
| BaseEnvelopeItem<SessionAggregatesItemHeaders, SessionAggregates>;
8182
export type ClientReportItem = BaseEnvelopeItem<ClientReportItemHeaders, ClientReport>;
8283
export type CheckInItem = BaseEnvelopeItem<CheckInItemHeaders, SerializedCheckIn>;

0 commit comments

Comments
 (0)