diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 5b2f74615c45..43c6227b0f8e 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -129,7 +129,6 @@ export class TraceService implements OnDestroy { if (!getActiveSpan()) { startBrowserTracingNavigationSpan(client, { name: strippedUrl, - op: 'navigation', origin: 'auto.navigation.angular', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index f4c47998ea90..9046ead7c7fc 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -11,7 +11,7 @@ import type { ExtendedBackburner } from '@sentry/ember/runloop'; import type { Span } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { BrowserClient } from '..'; import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; @@ -115,17 +115,18 @@ export function _instrumentEmberRouter( browserTracingOptions.instrumentPageLoad !== false ) { const routeInfo = routerService.recognize(url); - Sentry.startBrowserTracingPageLoadSpan(client, { + activeRootSpan = Sentry.startBrowserTracingPageLoadSpan(client, { name: `route:${routeInfo.name}`, - op: 'pageload', origin: 'auto.pageload.ember', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, tags: { url, toRoute: routeInfo.name, 'routing.instrumentation': '@sentry/ember', }, }); - activeRootSpan = getActiveSpan(); } const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => { @@ -147,10 +148,12 @@ export function _instrumentEmberRouter( const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); activeRootSpan?.end(); - Sentry.startBrowserTracingNavigationSpan(client, { + activeRootSpan = Sentry.startBrowserTracingNavigationSpan(client, { name: `route:${toRoute}`, - op: 'navigation', origin: 'auto.navigation.ember', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, tags: { fromRoute, toRoute, @@ -158,8 +161,6 @@ export function _instrumentEmberRouter( }, }); - activeRootSpan = getActiveSpan(); - transitionSpan = startInactiveSpan({ attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 31660eff00a7..b27575e147cf 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,5 +1,6 @@ -/* eslint-disable max-lines, complexity */ +/* eslint-disable max-lines */ import type { IdleTransaction } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -237,7 +238,6 @@ export const browserTracingIntegration = ((_options: Partial { @@ -325,7 +328,10 @@ export const browserTracingIntegration = ((_options: Partial { + // Clean up JSDom + Object.defineProperty(WINDOW, 'document', { value: originalGlobalDocument }); + Object.defineProperty(WINDOW, 'location', { value: originalGlobalLocation }); + Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory }); +}); + +describe('browserTracingIntegration', () => { + afterEach(() => { + getCurrentScope().clear(); + }); + + it('works with tracing enabled', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(true); + expect(spanToJSON(span!)).toEqual({ + description: '/', + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + + it('works with tracing disabled', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with tracing enabled but unsampled', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + conditionalTest({ min: 10 })('navigation', () => { + it('starts navigation when URL changes', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(true); + expect(span!.isRecording()).toBe(true); + expect(spanToJSON(span!)).toEqual({ + description: '/', + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + // this is what is used to get the span name - JSDOM does not update this on it's own! + const dom = new JSDOM(undefined, { url: 'https://example.com/test' }); + Object.defineProperty(global, 'location', { value: dom.window.document.location, writable: true }); + + WINDOW.history.pushState({}, '', '/test'); + + expect(span!.isRecording()).toBe(false); + + const span2 = getActiveSpan(); + expect(span2).toBeDefined(); + expect(spanIsSampled(span2!)).toBe(true); + expect(span2!.isRecording()).toBe(true); + expect(spanToJSON(span2!)).toEqual({ + description: '/test', + op: 'navigation', + origin: 'auto.navigation.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + // this is what is used to get the span name - JSDOM does not update this on it's own! + const dom2 = new JSDOM(undefined, { url: 'https://example.com/test2' }); + Object.defineProperty(global, 'location', { value: dom2.window.document.location, writable: true }); + + WINDOW.history.pushState({}, '', '/test2'); + + expect(span2!.isRecording()).toBe(false); + + const span3 = getActiveSpan(); + expect(span3).toBeDefined(); + expect(spanIsSampled(span3!)).toBe(true); + expect(span3!.isRecording()).toBe(true); + expect(spanToJSON(span3!)).toEqual({ + description: '/test2', + op: 'navigation', + origin: 'auto.navigation.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); + + describe('startBrowserTracingPageLoadSpan', () => { + it('works without integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeUndefined(); + }); + + it('works with unsampled span', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'pageload', + origin: 'manual', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + expect(spanIsSampled(span!)).toBe(true); + }); + + it('allows to overwrite properties', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { + name: 'test span', + origin: 'auto.test', + attributes: { testy: 'yes' }, + }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'pageload', + origin: 'auto.test', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + testy: 'yes', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); + + describe('startBrowserTracingNavigationSpan', () => { + it('works without integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeUndefined(); + }); + + it('works with unsampled span', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'navigation', + origin: 'manual', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + expect(spanIsSampled(span!)).toBe(true); + }); + + it('allows to overwrite properties', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { + name: 'test span', + origin: 'auto.test', + attributes: { testy: 'yes' }, + }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'navigation', + origin: 'auto.test', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + testy: 'yes', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); +}); diff --git a/packages/tracing-internal/test/utils/utils.ts b/packages/tracing-internal/test/utils/utils.ts new file mode 100644 index 000000000000..0652be303ed4 --- /dev/null +++ b/packages/tracing-internal/test/utils/utils.ts @@ -0,0 +1,20 @@ +import { parseSemver } from '@sentry/utils'; + +export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; + +/** + * Returns`describe` or `describe.skip` depending on allowed major versions of Node. + * + * @param {{ min?: number; max?: number }} allowedVersion + * @return {*} {jest.Describe} + */ +export const conditionalTest = (allowedVersion: { min?: number; max?: number }): jest.Describe => { + const major = NODE_VERSION.major; + if (!major) { + return describe.skip as jest.Describe; + } + + return major < (allowedVersion.min || -Infinity) || major > (allowedVersion.max || Infinity) + ? (describe.skip as jest.Describe) + : (describe as any); +};