diff --git a/.size-limit.js b/.size-limit.js index 4f86a9f8a2ea..f4bf45b47b40 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -15,7 +15,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init'), gzip: true, - limit: '24.1 KB', + limit: '24.5 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); @@ -190,7 +190,7 @@ module.exports = [ name: 'CDN Bundle (incl. Logs, Metrics)', path: createCDNPath('bundle.logs.metrics.min.js'), gzip: true, - limit: '29 KB', + limit: '30 KB', }, { name: 'CDN Bundle (incl. Tracing, Logs, Metrics)', @@ -214,7 +214,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics)', path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: true, - limit: '81 KB', + limit: '82 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback)', @@ -241,7 +241,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '128 KB', + limit: '129 KB', }, { name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed', diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d01dd3117e..599f5ff26c12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +### Important Changes + - **feat(tanstackstart-react): Add global sentry exception middlewares ([#19330](https://github.com/getsentry/sentry-javascript/pull/19330))** The `sentryGlobalRequestMiddleware` and `sentryGlobalFunctionMiddleware` global middlewares capture unhandled exceptions thrown in TanStack Start API routes and server functions. Add them as the first entries in the `requestMiddleware` and `functionMiddleware` arrays of `createStart()`: @@ -18,11 +20,21 @@ }); ``` -### Important Changes +- **fix(node-core): Reduce bundle size by removing apm-js-collab and requiring pino >= 9.10 ([#18631](https://github.com/getsentry/sentry-javascript/pull/18631))** + + In order to keep receiving pino logs, you need to update your pino version to >= 9.10, the reason for the support bump is to reduce the bundle size of the node-core SDK in frameworks that cannot tree-shake the apm-js-collab dependency. + +- **fix(browser): Ensure user id is consistently added to sessions ([#19341](https://github.com/getsentry/sentry-javascript/pull/19341))** -- fix(node-core): Reduce bundle size by removing apm-js-collab and requiring pino >= 9.10 ([#18631](https://github.com/getsentry/sentry-javascript/pull/18631)) + Previously, the SDK inconsistently set the user id on sessions, meaning sessions were often lacking proper coupling to the user set for example via `Sentry.setUser()`. + Additionally, the SDK incorrectly skipped starting a new session for the first soft navigation after the pageload. + This patch fixes these issues. As a result, metrics around sessions, like "Crash Free Sessions" or "Crash Free Users" might change. + This could also trigger alerts, depending on your set thresholds and conditions. + We apologize for any inconvenience caused! -In order to keep receiving pino logs, you need to update your pino version to >= 9.10, the reason for the support bump is to reduce the bundle size of the node-core SDK in frameworks that cannot tree-shake the apm-js-collab dependency. + While we're at it, if you're using Sentry in a Single Page App or meta framework, you might want to give the new `'page'` session lifecycle a try! + This new mode no longer creates a session per soft navigation but continues the initial session until the next hard page refresh. + Check out the [docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/) to learn more! ## 10.39.0 diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html index 291d2f23499b..e7c463998c02 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html @@ -4,6 +4,6 @@ - Navigate + Navigate diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts index 781e22c28f40..c7bb2c571f2d 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts @@ -1,31 +1,42 @@ -import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { waitForSession } from '../../../utils/helpers'; -sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl, page }) => { +sentryTest('starts a new session on pageload.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const session = await getFirstSentryEnvelopeRequest(page, url); + const sessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + + await page.goto(url); + const session = await sessionPromise; expect(session).toBeDefined(); - expect(session.init).toBe(true); - expect(session.errors).toBe(0); - expect(session.status).toBe('ok'); - expect(session.did).toBe('1337'); + expect(session).toEqual({ + attrs: { + environment: 'production', + release: '0.1', + user_agent: expect.any(String), + }, + did: '1337', + errors: 0, + init: true, + sid: expect.any(String), + started: expect.any(String), + status: 'ok', + timestamp: expect.any(String), + }); }); -sentryTest('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => { +sentryTest('starts a new session with navigation.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); + const initSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); - // Route must be set up before any navigation to avoid race conditions - await page.route('**/foo', (route: Route) => route.continue({ url })); - - const initSession = await getFirstSentryEnvelopeRequest(page, url); + await page.goto(url); + const initSession = await initSessionPromise; + const newSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); await page.locator('#navigate').click(); - const newSession = await getFirstSentryEnvelopeRequest(page, url); + const newSession = await newSessionPromise; expect(newSession).toBeDefined(); expect(newSession.init).toBe(true); diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/subject.js b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/subject.js index 11d2e4fd8ada..812e436a0a1f 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/subject.js +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/subject.js @@ -10,3 +10,7 @@ document.getElementById('manual-session').addEventListener('click', () => { Sentry.startSession(); Sentry.captureException('Test error from manual session'); }); + +document.getElementById('error').addEventListener('click', () => { + throw new Error('Test error from error button'); +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/template.html b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/template.html index 3e0d0aefc7df..994ca54735c6 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/template.html +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/template.html @@ -6,5 +6,6 @@ + diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts index 1837393f86cc..869b194031cf 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts @@ -1,45 +1,53 @@ import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; +import type { SerializedSession } from '@sentry/core/src'; import { sentryTest } from '../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; +import { + envelopeRequestParser, + getMultipleSentryEnvelopeRequests, + waitForErrorRequest, + waitForSession, +} from '../../../utils/helpers'; -sentryTest('should start a session on pageload with page lifecycle.', async ({ getLocalTestUrl, page }) => { +sentryTest('starts a session on pageload with page lifecycle.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const sessions = await getMultipleSentryEnvelopeRequests(page, 1, { - url, - envelopeType: 'session', - timeout: 2000, - }); + const sessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + await page.goto(url); + const session = await sessionPromise; - expect(sessions.length).toBeGreaterThanOrEqual(1); - const session = sessions[0]; expect(session).toBeDefined(); - expect(session.init).toBe(true); - expect(session.errors).toBe(0); - expect(session.status).toBe('ok'); + expect(session).toEqual({ + attrs: { + environment: 'production', + release: '0.1', + user_agent: expect.any(String), + }, + errors: 0, + init: true, + sid: expect.any(String), + started: expect.any(String), + status: 'ok', + timestamp: expect.any(String), + }); }); sentryTest( - 'should NOT start a new session on pushState navigation with page lifecycle.', + "doesn't start a new session on pushState navigation with page lifecycle.", async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { + const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { url, envelopeType: 'session', timeout: 4000, }); - const manualSessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { + const manualSessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { envelopeType: 'session', timeout: 4000, }); - const eventsPromise = getMultipleSentryEnvelopeRequests(page, 10, { - envelopeType: 'event', - timeout: 4000, - }); + const eventsPromise = waitForErrorRequest(page, e => e.message === 'Test error from manual session'); await page.waitForSelector('#navigate'); @@ -56,17 +64,42 @@ sentryTest( await page.locator('#manual-session').click(); const newSessions = (await manualSessionsPromise).filter(session => session.init); - const events = await eventsPromise; + const event = envelopeRequestParser(await eventsPromise); expect(newSessions.length).toBe(2); expect(newSessions[0].init).toBe(true); expect(newSessions[1].init).toBe(true); expect(newSessions[1].sid).not.toBe(newSessions[0].sid); - expect(events).toEqual([ + expect(event).toEqual( expect.objectContaining({ level: 'error', message: 'Test error from manual session', }), - ]); + ); }, ); + +sentryTest('Updates the session when an error is thrown', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const initialSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + await page.goto(url); + const initialSession = await initialSessionPromise; + + // for good measure, throw in a few navigations + await page.locator('#navigate').click(); + await page.locator('#navigate').click(); + await page.locator('#navigate').click(); + + const updatedSessionPromise = waitForSession(page, s => !s.init && s.status !== 'ok'); + await page.locator('#error').click(); + const updatedSession = await updatedSessionPromise; + + expect(updatedSession).toEqual({ + ...initialSession, + errors: 1, + init: false, + status: 'crashed', + timestamp: expect.any(String), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts index 4f4f0641feeb..1ccee9cd2728 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; +import type { SerializedSession } from '@sentry/core/src'; import { sentryTest } from '../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; @@ -8,7 +8,7 @@ sentryTest( async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { + const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, { url, envelopeType: 'session', timeout: 4000, @@ -20,8 +20,8 @@ sentryTest( await page.locator('#navigate').click(); await page.locator('#navigate').click(); - const sessions = (await sessionsPromise).filter(session => session.init); + const startedSessions = (await sessionsPromise).filter(session => session.init); - expect(sessions.length).toBe(3); + expect(startedSessions.length).toBe(4); }, ); diff --git a/dev-packages/browser-integration-tests/suites/sessions/start-session/template.html b/dev-packages/browser-integration-tests/suites/sessions/start-session/template.html index 291d2f23499b..e7c463998c02 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/start-session/template.html +++ b/dev-packages/browser-integration-tests/suites/sessions/start-session/template.html @@ -4,6 +4,6 @@ - Navigate + Navigate diff --git a/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts index 063fcb3dbd13..d5a0d3e7837a 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts @@ -1,12 +1,12 @@ -import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { waitForSession } from '../../../utils/helpers'; sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const session = await getFirstSentryEnvelopeRequest(page, url); + const sessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + await page.goto(url); + const session = await sessionPromise; expect(session).toBeDefined(); expect(session.init).toBe(true); @@ -16,18 +16,20 @@ sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl, sentryTest('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - await page.route('**/foo', (route: Route) => route.continue({ url })); - const initSession = await getFirstSentryEnvelopeRequest(page, url); + const initSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + await page.goto(url); + const initSession = await initSessionPromise; + const newSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); await page.locator('#navigate').click(); - - const newSession = await getFirstSentryEnvelopeRequest(page, url); + const newSession = await newSessionPromise; expect(newSession).toBeDefined(); expect(newSession.init).toBe(true); expect(newSession.errors).toBe(0); expect(newSession.status).toBe('ok'); expect(newSession.sid).toBeDefined(); + expect(initSession.sid).not.toBe(newSession.sid); }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts index 816cdf1dc056..96c68858b361 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts @@ -1,21 +1,22 @@ import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, waitForSession } from '../../../utils/helpers'; +import { waitForSession } from '../../../utils/helpers'; sentryTest('should update session when an error is thrown.', async ({ getLocalTestUrl, page }) => { + const pageloadSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadSession = await getFirstSentryEnvelopeRequest(page, url); + await page.goto(url); + const pageloadSession = await pageloadSessionPromise; - const updatedSessionPromise = waitForSession(page); + const updatedSessionPromise = waitForSession(page, s => !s.init); await page.locator('#throw-error').click(); const updatedSession = await updatedSessionPromise; expect(pageloadSession).toBeDefined(); expect(pageloadSession.init).toBe(true); expect(pageloadSession.errors).toBe(0); - expect(updatedSession).toBeDefined(); + expect(updatedSession.init).toBe(false); expect(updatedSession.errors).toBe(1); expect(updatedSession.status).toBe('crashed'); @@ -25,7 +26,9 @@ sentryTest('should update session when an error is thrown.', async ({ getLocalTe sentryTest('should update session when an exception is captured.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadSession = await getFirstSentryEnvelopeRequest(page, url); + const pageloadSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + await page.goto(url); + const pageloadSession = await pageloadSessionPromise; const updatedSessionPromise = waitForSession(page); await page.locator('#capture-exception').click(); @@ -34,6 +37,7 @@ sentryTest('should update session when an exception is captured.', async ({ getL expect(pageloadSession).toBeDefined(); expect(pageloadSession.init).toBe(true); expect(pageloadSession.errors).toBe(0); + expect(updatedSession).toBeDefined(); expect(updatedSession.init).toBe(false); expect(updatedSession.errors).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/sessions/user/subject.js b/dev-packages/browser-integration-tests/suites/sessions/user/subject.js new file mode 100644 index 000000000000..16d9653f0b7b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/user/subject.js @@ -0,0 +1,5 @@ +Sentry.setUser({ + id: '1337', + email: 'user@name.com', + username: 'user1337', +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/user/template.html b/dev-packages/browser-integration-tests/suites/sessions/user/template.html new file mode 100644 index 000000000000..e7c463998c02 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/user/template.html @@ -0,0 +1,9 @@ + + + + + + + Navigate + + diff --git a/dev-packages/browser-integration-tests/suites/sessions/user/test.ts b/dev-packages/browser-integration-tests/suites/sessions/user/test.ts new file mode 100644 index 000000000000..f9ba096356ce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/user/test.ts @@ -0,0 +1,90 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { waitForSession } from '../../../utils/helpers'; + +sentryTest('updates the session when setting the user', async ({ getLocalTestUrl, page }) => { + const initialSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + const updatedSessionPromise = waitForSession(page, s => !s.init && s.status === 'ok'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const initialSession = await initialSessionPromise; + const updatedSession = await updatedSessionPromise; + + expect(initialSession).toEqual({ + attrs: { + environment: 'production', + release: '0.1', + user_agent: expect.any(String), + }, + errors: 0, + init: true, + sid: expect.any(String), + started: expect.any(String), + status: 'ok', + timestamp: expect.any(String), + }); + + expect(updatedSession).toEqual({ + ...initialSession, + init: false, + timestamp: expect.any(String), + did: '1337', + }); +}); + +sentryTest('includes the user id in the exited session', async ({ getLocalTestUrl, page }) => { + const initialSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const initialSession = await initialSessionPromise; + + const exitedInitialSessionPromise = waitForSession(page, s => !s.init && s.status === 'exited'); + + await page.locator('#navigate').click(); + + const exitedInitialSession = await exitedInitialSessionPromise; + + expect(exitedInitialSession).toEqual({ + ...initialSession, + timestamp: expect.any(String), + init: false, + status: 'exited', + did: '1337', + }); +}); + +sentryTest('includes the user id in the subsequent session', async ({ getLocalTestUrl, page }) => { + const initialSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const initialSession = await initialSessionPromise; + + expect(initialSession).toEqual({ + attrs: { + environment: 'production', + release: '0.1', + user_agent: expect.any(String), + }, + errors: 0, + init: true, + sid: expect.any(String), + started: expect.any(String), + status: 'ok', + timestamp: expect.any(String), + }); + + const secondSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok' && s.sid !== initialSession.sid); + + await page.locator('#navigate').click(); + + const secondSession = await secondSessionPromise; + + expect(secondSession.sid).not.toBe(initialSession.sid); + expect(secondSession.did).toBe('1337'); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index ae534463fe80..9da79bf15b0b 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -202,6 +202,7 @@ class SentryScenarioGenerationPlugin { factory.hooks.parser.for('javascript/auto').tap(this._name, parser => { parser.hooks.import.tap( this._name, + // @ts-expect-error - not sure why this is failing suddenly??? (statement: { specifiers: [{ imported: { name: string } }] }, source: string) => { const imported = statement.specifiers?.[0]?.imported?.name; diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 6cc5188d3c29..50150c6bee20 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -8,7 +8,7 @@ import type { Event as SentryEvent, EventEnvelope, EventEnvelopeHeaders, - SessionContext, + SerializedSession, TransactionEvent, } from '@sentry/core'; import { parseEnvelope } from '@sentry/core'; @@ -283,7 +283,10 @@ export function waitForClientReportRequest(page: Page, callback?: (report: Clien }); } -export async function waitForSession(page: Page): Promise { +export async function waitForSession( + page: Page, + callback?: (session: SerializedSession) => boolean, +): Promise { const req = await page.waitForRequest(req => { const postData = req.postData(); if (!postData) { @@ -291,7 +294,11 @@ export async function waitForSession(page: Page): Promise { } try { - const event = envelopeRequestParser(req); + const event = envelopeRequestParser(req); + + if (callback) { + return callback(event); + } return typeof event.init === 'boolean' && event.started !== undefined; } catch { @@ -299,7 +306,7 @@ export async function waitForSession(page: Page): Promise { } }); - return envelopeRequestParser(req); + return envelopeRequestParser(req); } /** diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc index 5ef4f1ff11f6..0b7b36047973 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc @@ -8,11 +8,10 @@ "SENTRY_DSN": "https://username@domain/123", "SENTRY_ENVIRONMENT": "qa", "SENTRY_TRACES_SAMPLE_RATE": "1.0", - "SENTRY_TUNNEL": "http://localhost:3031/" + "SENTRY_TUNNEL": "http://localhost:3031/", }, "assets": { "binding": "ASSETS", - "directory": "./dist" - } + "directory": "./dist", + }, } - diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts index a0eb63034b9f..23fdb4087af9 100644 --- a/packages/browser/src/integrations/browsersession.ts +++ b/packages/browser/src/integrations/browsersession.ts @@ -1,4 +1,4 @@ -import { captureSession, debug, defineIntegration, startSession } from '@sentry/core'; +import { captureSession, debug, defineIntegration, getIsolationScope, startSession } from '@sentry/core'; import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; @@ -43,11 +43,32 @@ export const browserSessionIntegration = defineIntegration((options: BrowserSess startSession({ ignoreDuration: true }); captureSession(); + // User data can be set at any time, for example async after Sentry.init has run and the initial session + // envelope was already sent, but still on the initial page. + // Therefore, we have to update the ongoing session with the new user data if it exists, to send the `did`. + // In theory, sessions, as well as user data is always put onto the isolation scope. So we listen to the + // isolation scope for changes and update the session with the new user data if it exists. + // This will not catch users set onto other scopes, like the current scope. For now, we'll accept this limitation. + // The alternative is to update and capture the session from within the scope. This could be too costly or would not + // play well with session aggregates on the server side. Since this happens in the scope class, we'd need change + // scope behaviour in the browser. + const isolationScope = getIsolationScope(); + let previousUser = isolationScope.getUser(); + isolationScope.addScopeListener(scope => { + const maybeNewUser = scope.getUser(); + // sessions only care about user id and ip address, so we only need to capture the session if the user has changed + if (previousUser?.id !== maybeNewUser?.id || previousUser?.ip_address !== maybeNewUser?.ip_address) { + // the scope class already writes the user to its session, so we only need to capture the session here + captureSession(); + previousUser = maybeNewUser; + } + }); + if (lifecycle === 'route') { // We want to create a session for every navigation as well addHistoryInstrumentationHandler(({ from, to }) => { // Don't create an additional session for the initial route or if the location did not change - if (from !== undefined && from !== to) { + if (from !== to) { startSession({ ignoreDuration: true }); captureSession(); } diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d7931565b7ab..7af14befc185 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -16,6 +16,7 @@ import { isThenable } from './utils/is'; import { uuid4 } from './utils/misc'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; +import { getCombinedScopeData } from './utils/scopeData'; import { timestampInSeconds } from './utils/time'; import { GLOBAL_OBJ } from './utils/worldwide'; @@ -270,13 +271,14 @@ export function addEventProcessor(callback: EventProcessor): void { */ export function startSession(context?: SessionContext): Session { const isolationScope = getIsolationScope(); - const currentScope = getCurrentScope(); + + const { user } = getCombinedScopeData(isolationScope, getCurrentScope()); // Will fetch userAgent if called from browser sdk const { userAgent } = GLOBAL_OBJ.navigator || {}; const session = makeSession({ - user: currentScope.getUser() || isolationScope.getUser(), + user, ...(userAgent && { userAgent }), ...context, });