Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init'),
gzip: true,
limit: '25.5 KB',
limit: '26 KB',
},
{
name: '@sentry/browser - with treeshaking flags',
Expand Down Expand Up @@ -148,7 +148,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '44.6 KB',
limit: '45 KB',
},
// Vue SDK (ESM)
{
Expand All @@ -163,22 +163,22 @@ 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)
{
name: '@sentry/svelte',
path: 'packages/svelte/build/esm/index.js',
import: createImport('init'),
gzip: true,
limit: '25.5 KB',
limit: '26 KB',
},
// Browser CDN bundles
{
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
limit: '28 KB',
limit: '29 KB',
},
{
name: 'CDN Bundle (incl. Tracing)',
Expand Down Expand Up @@ -234,7 +234,7 @@ module.exports = [
path: createCDNPath('bundle.min.js'),
gzip: false,
brotli: false,
limit: '82 KB',
limit: '83 KB',
},
{
name: 'CDN Bundle (incl. Tracing) - uncompressed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ sentryTest('should capture feedback with custom button', async ({ getLocalTestUr
type: 'feedback',
breadcrumbs: expect.any(Array),
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
feedback: {
contact_email: '[email protected]',
message: 'my example feedback',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ sentryTest('should capture feedback', async ({ getLocalTestUrl, page }) => {
type: 'feedback',
breadcrumbs: expect.any(Array),
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
feedback: {
contact_email: '[email protected]',
message: 'my example feedback',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ sentryTest('should capture feedback', async ({ forceFlushReplay, getLocalTestUrl
type: 'feedback',
breadcrumbs: expect.any(Array),
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
feedback: {
contact_email: '[email protected]',
message: 'my example feedback',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ sentryTest('should capture feedback', async ({ getLocalTestUrl, page }) => {
type: 'feedback',
breadcrumbs: expect.any(Array),
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
feedback: {
contact_email: '[email protected]',
message: 'my example feedback',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

const integrations = Sentry.getDefaultIntegrations({}).filter(
defaultIntegration => defaultIntegration.name === 'CultureContext',
);

const client = new Sentry.BrowserClient({
dsn: 'https://[email protected]/1337',
transport: Sentry.makeFetchTransport,
stackParser: Sentry.defaultStackParser,
integrations: integrations,
});

const scope = new Sentry.Scope();
scope.setClient(client);
client.init();

window._sentryScope = scope;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window._sentryScope.captureException(new Error('test error'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';

sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData.contexts?.culture).toEqual({
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
'Sentry Logger [log]: Integration installed: LinkedErrors',
'Sentry Logger [log]: Integration installed: Dedupe',
'Sentry Logger [log]: Integration installed: HttpContext',
'Sentry Logger [log]: Integration installed: CultureContext',
'Sentry Logger [warn]: Discarded session because of missing or non-string release',
'Sentry Logger [log]: Integration installed: BrowserSession',
'test log',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ sentryTest(
});

expect(transactionEnvelopeItem).toEqual({
contexts: {
contexts: expect.objectContaining({
trace: {
data: {
'sentry.origin': 'manual',
Expand All @@ -94,7 +94,7 @@ sentryTest(
span_id: parentSpanId,
trace_id: traceId,
},
},
}),
environment: 'production',
event_id: expect.any(String),
platform: 'javascript',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT
replay_type: 'session',
event_id: expect.stringMatching(/\w{32}/),
environment: 'production',
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
},
sdk: {
integrations: expect.arrayContaining([
'InboundFilters',
Expand Down Expand Up @@ -73,6 +80,13 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT
replay_type: 'session',
event_id: expect.stringMatching(/\w{32}/),
environment: 'production',
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
},
sdk: {
integrations: expect.arrayContaining([
'InboundFilters',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g
replay_type: 'session',
event_id: expect.stringMatching(/\w{32}/),
environment: 'production',
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
},
sdk: {
integrations: expect.arrayContaining([
'InboundFilters',
Expand Down Expand Up @@ -73,6 +80,13 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g
replay_type: 'session',
event_id: expect.stringMatching(/\w{32}/),
environment: 'production',
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
},
sdk: {
integrations: expect.arrayContaining([
'InboundFilters',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const DEFAULT_REPLAY_EVENT = {
replay_type: 'session',
event_id: expect.stringMatching(/\w{32}/),
environment: 'production',
contexts: {
culture: {
locale: expect.any(String),
timezone: expect.any(String),
calendar: expect.any(String),
},
},
sdk: {
integrations: expect.arrayContaining([
'InboundFilters',
Expand All @@ -25,6 +32,7 @@ const DEFAULT_REPLAY_EVENT = {
'LinkedErrors',
'Dedupe',
'HttpContext',
'CultureContext',
'BrowserSession',
'Replay',
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('Sends a pageload transaction', async ({ page }) => {
transaction: '/',
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
contexts: expect.objectContaining({
react: {
version: expect.any(String),
},
Expand All @@ -40,7 +40,7 @@ test('Sends a pageload transaction', async ({ page }) => {
'sentry.source': 'url',
}),
},
},
}),
request: {
headers: {
'User-Agent': expect.any(String),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('Sends a pageload transaction', async ({ page }) => {
transaction: '/',
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
contexts: expect.objectContaining({
react: {
version: expect.any(String),
},
Expand All @@ -40,7 +40,7 @@ test('Sends a pageload transaction', async ({ page }) => {
'sentry.source': 'route',
}),
},
},
}),
request: {
headers: {
'User-Agent': expect.any(String),
Expand Down Expand Up @@ -70,7 +70,7 @@ test('Sends a navigation transaction', async ({ page }) => {
transaction: '/user/[id]',
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
contexts: expect.objectContaining({
react: {
version: expect.any(String),
},
Expand Down Expand Up @@ -98,7 +98,7 @@ test('Sends a navigation transaction', async ({ page }) => {
},
],
},
},
}),
request: {
headers: {
'User-Agent': expect.any(String),
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BrowserOptions } from '@sentry/browser';
import {
breadcrumbsIntegration,
browserSessionIntegration,
cultureContextIntegration,
globalHandlersIntegration,
httpContextIntegration,
init as browserInit,
Expand Down Expand Up @@ -43,6 +44,7 @@ export function getDefaultIntegrations(_options: BrowserOptions = {}): Integrati
linkedErrorsIntegration(),
dedupeIntegration(),
httpContextIntegration(),
cultureContextIntegration(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Is that the only sdk where we need to add this separately?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I checked the other SDKs and they don't override the browser's default integrations. Only the angular one does.

browserSessionIntegration(),
];
}
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type { Span, FeatureFlagsIntegration } from '@sentry/core';
export { makeBrowserOfflineTransport } from './transports/offline';
export { browserProfilingIntegration } from './profiling/integration';
export { spotlightBrowserIntegration } from './integrations/spotlight';
export { cultureContextIntegration } from './integrations/culturecontext';
export { browserSessionIntegration } from './integrations/browsersession';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
Expand Down
60 changes: 60 additions & 0 deletions packages/browser/src/integrations/culturecontext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { CultureContext, IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { WINDOW } from '../helpers';

const INTEGRATION_NAME = 'CultureContext';

const _cultureContextIntegration = (() => {
return {
name: INTEGRATION_NAME,
preprocessEvent(event) {
const culture = getCultureContext();

if (culture) {
event.contexts = {
...event.contexts,
culture: { ...culture, ...event.contexts?.culture },
};
}
},
};
}) satisfies IntegrationFn;

/**
* Captures culture context from the browser.
*
* Enabled by default.
*
* @example
* ```js
* import * as Sentry from '@sentry/browser';
*
* Sentry.init({
* integrations: [Sentry.cultureContextIntegration()],
* });
* ```
*/
export const cultureContextIntegration = defineIntegration(_cultureContextIntegration);

/**
* Returns the culture context from the browser's Intl API.
*/
function getCultureContext(): CultureContext | undefined {
try {
const intl = (WINDOW as { Intl?: typeof Intl }).Intl;
if (!intl) {
return undefined;
}

const options = intl.DateTimeFormat().resolvedOptions();

return {
locale: options.locale,
timezone: options.timeZone,
calendar: options.calendar,
};
} catch {
// Ignore errors
Comment on lines +54 to +57
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calendar property from resolvedOptions() is included here but is not included in the Node.js implementation (packages/node-core/src/integrations/context.ts:190-193). Consider checking whether calendar is consistently available across all browsers, as it may not be present in older browsers or certain browser configurations. If consistency with the Node.js implementation is desired, or if browser compatibility is a concern, the calendar property could be made optional or omitted. The Node.js implementation only includes locale and timezone.

Suggested change
calendar: options.calendar,
};
} catch {
// Ignore errors
};
} catch {
// Ignore errors
// Ignore errors

Copilot uses AI. Check for mistakes.
return undefined;
}
}
Loading
Loading