diff --git a/.size-limit.js b/.size-limit.js
index 7f8e1809522f..10abf4062d7d 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -163,7 +163,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
- limit: '44.1 KB',
+ limit: '45 KB',
},
// Svelte SDK (ESM)
{
@@ -178,7 +178,7 @@ module.exports = [
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
- limit: '28 KB',
+ limit: '29 KB',
},
{
name: 'CDN Bundle (incl. Tracing)',
diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js
new file mode 100644
index 000000000000..6452dbf515f9
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '0.1',
+ integrations: [Sentry.browserSessionIntegration({ lifecycle: 'page' })],
+});
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
new file mode 100644
index 000000000000..11d2e4fd8ada
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/subject.js
@@ -0,0 +1,12 @@
+let clickCount = 0;
+
+document.getElementById('navigate').addEventListener('click', () => {
+ clickCount++;
+ // Each click navigates to a different page
+ history.pushState({}, '', `/page-${clickCount}`);
+});
+
+document.getElementById('manual-session').addEventListener('click', () => {
+ Sentry.startSession();
+ Sentry.captureException('Test error from manual session');
+});
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
new file mode 100644
index 000000000000..0677aeffb4a9
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000000..1837393f86cc
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts
@@ -0,0 +1,72 @@
+import { expect } from '@playwright/test';
+import type { SessionContext } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers';
+
+sentryTest('should start 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,
+ });
+
+ 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');
+});
+
+sentryTest(
+ 'should NOT start a new session on pushState navigation with page lifecycle.',
+ async ({ getLocalTestUrl, page }) => {
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, {
+ url,
+ envelopeType: 'session',
+ timeout: 4000,
+ });
+
+ const manualSessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, {
+ envelopeType: 'session',
+ timeout: 4000,
+ });
+
+ const eventsPromise = getMultipleSentryEnvelopeRequests(page, 10, {
+ envelopeType: 'event',
+ timeout: 4000,
+ });
+
+ await page.waitForSelector('#navigate');
+
+ await page.locator('#navigate').click();
+ await page.locator('#navigate').click();
+ await page.locator('#navigate').click();
+
+ const sessions = (await sessionsPromise).filter(session => session.init);
+
+ expect(sessions.length).toBe(1);
+ expect(sessions[0].init).toBe(true);
+
+ // teardown and verify if nothing else got sent
+ await page.locator('#manual-session').click();
+
+ const newSessions = (await manualSessionsPromise).filter(session => session.init);
+ const events = 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.objectContaining({
+ level: 'error',
+ message: 'Test error from manual session',
+ }),
+ ]);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js
new file mode 100644
index 000000000000..af2df91a7ceb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '0.1',
+});
diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/subject.js b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/subject.js
new file mode 100644
index 000000000000..41f372c3aa79
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/subject.js
@@ -0,0 +1,6 @@
+let clickCount = 0;
+
+document.getElementById('navigate').addEventListener('click', () => {
+ clickCount++;
+ history.pushState({}, '', `/page-${clickCount}`);
+});
diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/template.html b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/template.html
new file mode 100644
index 000000000000..2a1b5d400981
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000000..4f4f0641feeb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts
@@ -0,0 +1,27 @@
+import { expect } from '@playwright/test';
+import type { SessionContext } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers';
+
+sentryTest(
+ 'should start new sessions on pushState navigation with route lifecycle (default).',
+ async ({ getLocalTestUrl, page }) => {
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, {
+ url,
+ envelopeType: 'session',
+ timeout: 4000,
+ });
+
+ await page.waitForSelector('#navigate');
+
+ await page.locator('#navigate').click();
+ await page.locator('#navigate').click();
+ await page.locator('#navigate').click();
+
+ const sessions = (await sessionsPromise).filter(session => session.init);
+
+ expect(sessions.length).toBe(3);
+ },
+);
diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts
index 9b733cff92bd..bf0de4783c5a 100644
--- a/packages/browser/src/exports.ts
+++ b/packages/browser/src/exports.ts
@@ -102,5 +102,6 @@ export { globalHandlersIntegration } from './integrations/globalhandlers';
export { httpContextIntegration } from './integrations/httpcontext';
export { linkedErrorsIntegration } from './integrations/linkederrors';
export { browserApiErrorsIntegration } from './integrations/browserapierrors';
+export { browserSessionIntegration } from './integrations/browsersession';
export { lazyLoadIntegration } from './utils/lazyLoadIntegration';
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 6e7c54198edc..db89b6110866 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -75,7 +75,6 @@ export type { Span, FeatureFlagsIntegration } from '@sentry/core';
export { makeBrowserOfflineTransport } from './transports/offline';
export { browserProfilingIntegration } from './profiling/integration';
export { spotlightBrowserIntegration } from './integrations/spotlight';
-export { browserSessionIntegration } from './integrations/browsersession';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
export { unleashIntegration } from './integrations/featureFlags/unleash';
diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts
index 78a9228e3b29..a0eb63034b9f 100644
--- a/packages/browser/src/integrations/browsersession.ts
+++ b/packages/browser/src/integrations/browsersession.ts
@@ -3,13 +3,30 @@ import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils
import { DEBUG_BUILD } from '../debug-build';
import { WINDOW } from '../helpers';
+interface BrowserSessionOptions {
+ /**
+ * Controls the session lifecycle - when new sessions are created.
+ *
+ * - `'route'`: A session is created on page load and on every navigation.
+ * This is the default behavior.
+ * - `'page'`: A session is created once when the page is loaded. Session is not
+ * updated on navigation. This is useful for webviews or single-page apps where
+ * URL changes should not trigger new sessions.
+ *
+ * @default 'route'
+ */
+ lifecycle?: 'route' | 'page';
+}
+
/**
* When added, automatically creates sessions which allow you to track adoption and crashes (crash free rate) in your Releases in Sentry.
* More information: https://docs.sentry.io/product/releases/health/
*
* Note: In order for session tracking to work, you need to set up Releases: https://docs.sentry.io/product/releases/
*/
-export const browserSessionIntegration = defineIntegration(() => {
+export const browserSessionIntegration = defineIntegration((options: BrowserSessionOptions = {}) => {
+ const lifecycle = options.lifecycle ?? 'route';
+
return {
name: 'BrowserSession',
setupOnce() {
@@ -26,14 +43,16 @@ export const browserSessionIntegration = defineIntegration(() => {
startSession({ ignoreDuration: true });
captureSession();
- // 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) {
- startSession({ ignoreDuration: true });
- captureSession();
- }
- });
+ 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) {
+ startSession({ ignoreDuration: true });
+ captureSession();
+ }
+ });
+ }
},
};
});