diff --git a/.size-limit.js b/.size-limit.js
index 978d3105dd41..c9961f9b5ee7 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -230,7 +230,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '130 KB',
+ limit: '131 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
diff --git a/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/init.js b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/init.js
new file mode 100644
index 000000000000..af2df91a7ceb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/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/navigation-mode/subject.js b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/subject.js
new file mode 100644
index 000000000000..41f372c3aa79
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/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/navigation-mode/template.html b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/template.html
new file mode 100644
index 000000000000..2a1b5d400981
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/test.ts b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/test.ts
new file mode 100644
index 000000000000..a9ab937a1816
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/navigation-mode/test.ts
@@ -0,0 +1,24 @@
+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 in default mode.', 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/dev-packages/browser-integration-tests/suites/sessions/single-mode/init.js b/dev-packages/browser-integration-tests/suites/sessions/single-mode/init.js
new file mode 100644
index 000000000000..890a54f166eb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/single-mode/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({ mode: 'single' })],
+});
diff --git a/dev-packages/browser-integration-tests/suites/sessions/single-mode/subject.js b/dev-packages/browser-integration-tests/suites/sessions/single-mode/subject.js
new file mode 100644
index 000000000000..97a4037f17bb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/single-mode/subject.js
@@ -0,0 +1,7 @@
+let clickCount = 0;
+
+document.getElementById('navigate').addEventListener('click', () => {
+ clickCount++;
+ // Each click navigates to a different page
+ history.pushState({}, '', `/page-${clickCount}`);
+});
diff --git a/dev-packages/browser-integration-tests/suites/sessions/single-mode/template.html b/dev-packages/browser-integration-tests/suites/sessions/single-mode/template.html
new file mode 100644
index 000000000000..2a1b5d400981
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/single-mode/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/sessions/single-mode/test.ts b/dev-packages/browser-integration-tests/suites/sessions/single-mode/test.ts
new file mode 100644
index 000000000000..c02c61eff450
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/sessions/single-mode/test.ts
@@ -0,0 +1,45 @@
+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 in single mode.', 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 in single mode.',
+ 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(1);
+ expect(sessions[0].init).toBe(true);
+ },
+);
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 6e7c54198edc..d24ec3cd63de 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -76,6 +76,7 @@ export { makeBrowserOfflineTransport } from './transports/offline';
export { browserProfilingIntegration } from './profiling/integration';
export { spotlightBrowserIntegration } from './integrations/spotlight';
export { browserSessionIntegration } from './integrations/browsersession';
+export type { BrowserSessionOptions } 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..10b6e0fcc05e 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';
+export interface BrowserSessionOptions {
+ /**
+ * Controls when sessions are created.
+ *
+ * - `'single'`: 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.
+ * - `'navigation'`: A session is created on page load and on every navigation.
+ * This is the default behavior.
+ *
+ * @default 'navigation'
+ */
+ mode?: 'single' | 'navigation';
+}
+
/**
* 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 mode = options.mode ?? 'navigation';
+
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 (mode === 'navigation') {
+ // 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();
+ }
+ });
+ }
},
};
});