Skip to content

Commit 6b8f76e

Browse files
authored
Merge branch 'develop' into timfish/feat/pino-integration
2 parents 687a619 + b24a7e6 commit 6b8f76e

File tree

8 files changed

+214
-25
lines changed

8 files changed

+214
-25
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
tracesSampleRate: 1,
8+
integrations: [
9+
Sentry.browserTracingIntegration({
10+
_experiments: { enableInteractions: true },
11+
}),
12+
],
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Clicking the navigate button will push a new history state, triggering navigation
2+
document.querySelector('[data-test-id=navigate-button]').addEventListener('click', () => {
3+
const loc = window.location;
4+
const url = loc.href.includes('#nav') ? loc.pathname : `${loc.pathname}#nav`;
5+
6+
history.pushState({}, '', url);
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button data-test-id="navigate-button">Navigate</button>
8+
</body>
9+
</html>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
4+
5+
sentryTest(
6+
'click-triggered navigation should produce a root navigation transaction',
7+
async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
await page.goto(url);
15+
await waitForTransactionRequest(page); // "pageload" root span
16+
17+
const interactionRequestPromise = waitForTransactionRequest(
18+
page,
19+
evt => evt.contexts?.trace?.op === 'ui.action.click',
20+
);
21+
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
22+
23+
await page.locator('[data-test-id=navigate-button]').click();
24+
25+
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
26+
const navigationEvent = envelopeRequestParser(await navigationRequestPromise);
27+
28+
// Navigation is root span, not a child span on the interaction
29+
expect(interactionEvent.contexts?.trace?.op).toBe('ui.action.click');
30+
expect(navigationEvent.contexts?.trace?.op).toBe('navigation');
31+
32+
expect(interactionEvent.contexts?.trace?.trace_id).not.toEqual(navigationEvent.contexts?.trace?.trace_id);
33+
34+
// does not contain a child navigation span
35+
const interactionSpans = interactionEvent.spans || [];
36+
const hasNavigationChild = interactionSpans.some(span => span.op === 'navigation' || span.op === 'http.server');
37+
expect(hasNavigationChild).toBeFalsy();
38+
},
39+
);

dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"dependencies": {
1414
"@sentry/cloudflare": "latest || *",
15-
"hono": "4.7.10"
15+
"hono": "4.9.7"
1616
},
1717
"devDependencies": {
1818
"@cloudflare/vitest-pool-workers": "^0.8.31",

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
536536
_createRouteSpan(client, {
537537
op: 'navigation',
538538
...startSpanOptions,
539+
// Navigation starts a new trace and is NOT parented under any active interaction (e.g. ui.action.click)
540+
parentSpan: null,
541+
forceTransaction: true,
539542
});
540543
});
541544

packages/core/src/eventProcessors.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Event, EventHint } from './types-hoist/event';
33
import type { EventProcessor } from './types-hoist/eventprocessor';
44
import { debug } from './utils/debug-logger';
55
import { isThenable } from './utils/is';
6-
import { SyncPromise } from './utils/syncpromise';
6+
import { rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise';
77

88
/**
99
* Process an array of event processors, returning the processed event (or `null` if the event was dropped).
@@ -14,24 +14,33 @@ export function notifyEventProcessors(
1414
hint: EventHint,
1515
index: number = 0,
1616
): PromiseLike<Event | null> {
17-
return new SyncPromise<Event | null>((resolve, reject) => {
18-
const processor = processors[index];
19-
if (event === null || typeof processor !== 'function') {
20-
resolve(event);
21-
} else {
22-
const result = processor({ ...event }, hint) as Event | null;
17+
try {
18+
const result = _notifyEventProcessors(event, hint, processors, index);
19+
return isThenable(result) ? result : resolvedSyncPromise(result);
20+
} catch (error) {
21+
return rejectedSyncPromise(error);
22+
}
23+
}
24+
25+
function _notifyEventProcessors(
26+
event: Event | null,
27+
hint: EventHint,
28+
processors: EventProcessor[],
29+
index: number,
30+
): Event | null | PromiseLike<Event | null> {
31+
const processor = processors[index];
32+
33+
if (!event || !processor) {
34+
return event;
35+
}
36+
37+
const result = processor({ ...event }, hint);
38+
39+
DEBUG_BUILD && result === null && debug.log(`Event processor "${processor.id || '?'}" dropped event`);
2340

24-
DEBUG_BUILD && processor.id && result === null && debug.log(`Event processor "${processor.id}" dropped event`);
41+
if (isThenable(result)) {
42+
return result.then(final => _notifyEventProcessors(final, hint, processors, index + 1));
43+
}
2544

26-
if (isThenable(result)) {
27-
void result
28-
.then(final => notifyEventProcessors(processors, final, hint, index + 1).then(resolve))
29-
.then(null, reject);
30-
} else {
31-
void notifyEventProcessors(processors, result, hint, index + 1)
32-
.then(resolve)
33-
.then(null, reject);
34-
}
35-
}
36-
});
45+
return _notifyEventProcessors(result, hint, processors, index + 1);
3746
}

packages/core/test/lib/client.test.ts

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,15 +1833,13 @@ describe('Client', () => {
18331833
});
18341834
});
18351835

1836-
test('event processor sends an event and logs when it crashes', () => {
1837-
expect.assertions(3);
1838-
1836+
test('event processor sends an event and logs when it crashes synchronously', () => {
18391837
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
18401838
const client = new TestClient(options);
18411839
const captureExceptionSpy = vi.spyOn(client, 'captureException');
18421840
const loggerWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn');
18431841
const scope = new Scope();
1844-
const exception = new Error('sorry');
1842+
const exception = new Error('sorry 1');
18451843
scope.addEventProcessor(() => {
18461844
throw exception;
18471845
});
@@ -1850,7 +1848,43 @@ describe('Client', () => {
18501848

18511849
expect(TestClient.instance!.event!.exception!.values![0]).toStrictEqual({
18521850
type: 'Error',
1853-
value: 'sorry',
1851+
value: 'sorry 1',
1852+
mechanism: { type: 'internal', handled: false },
1853+
});
1854+
expect(captureExceptionSpy).toBeCalledWith(exception, {
1855+
data: {
1856+
__sentry__: true,
1857+
},
1858+
originalException: exception,
1859+
mechanism: { type: 'internal', handled: false },
1860+
});
1861+
expect(loggerWarnSpy).toBeCalledWith(
1862+
`Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${exception}`,
1863+
);
1864+
});
1865+
1866+
test('event processor sends an event and logs when it crashes asynchronously', async () => {
1867+
vi.useFakeTimers();
1868+
1869+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1870+
const client = new TestClient(options);
1871+
const captureExceptionSpy = vi.spyOn(client, 'captureException');
1872+
const loggerWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn');
1873+
const scope = new Scope();
1874+
const exception = new Error('sorry 2');
1875+
scope.addEventProcessor(() => {
1876+
return new Promise((_resolve, reject) => {
1877+
reject(exception);
1878+
});
1879+
});
1880+
1881+
client.captureEvent({ message: 'hello' }, {}, scope);
1882+
1883+
await vi.runOnlyPendingTimersAsync();
1884+
1885+
expect(TestClient.instance!.event!.exception!.values![0]).toStrictEqual({
1886+
type: 'Error',
1887+
value: 'sorry 2',
18541888
mechanism: { type: 'internal', handled: false },
18551889
});
18561890
expect(captureExceptionSpy).toBeCalledWith(exception, {
@@ -1865,6 +1899,81 @@ describe('Client', () => {
18651899
);
18661900
});
18671901

1902+
test('event processor sends an event and logs when it crashes synchronously in processor chain', () => {
1903+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1904+
const client = new TestClient(options);
1905+
const captureExceptionSpy = vi.spyOn(client, 'captureException');
1906+
const scope = new Scope();
1907+
const exception = new Error('sorry 3');
1908+
1909+
const processor1 = vi.fn(event => {
1910+
return event;
1911+
});
1912+
const processor2 = vi.fn(() => {
1913+
throw exception;
1914+
});
1915+
const processor3 = vi.fn(event => {
1916+
return event;
1917+
});
1918+
1919+
scope.addEventProcessor(processor1);
1920+
scope.addEventProcessor(processor2);
1921+
scope.addEventProcessor(processor3);
1922+
1923+
client.captureEvent({ message: 'hello' }, {}, scope);
1924+
1925+
expect(processor1).toHaveBeenCalledTimes(1);
1926+
expect(processor2).toHaveBeenCalledTimes(1);
1927+
expect(processor3).toHaveBeenCalledTimes(0);
1928+
1929+
expect(captureExceptionSpy).toBeCalledWith(exception, {
1930+
data: {
1931+
__sentry__: true,
1932+
},
1933+
originalException: exception,
1934+
mechanism: { type: 'internal', handled: false },
1935+
});
1936+
});
1937+
1938+
test('event processor sends an event and logs when it crashes asynchronously in processor chain', async () => {
1939+
vi.useFakeTimers();
1940+
1941+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1942+
const client = new TestClient(options);
1943+
const captureExceptionSpy = vi.spyOn(client, 'captureException');
1944+
const scope = new Scope();
1945+
const exception = new Error('sorry 4');
1946+
1947+
const processor1 = vi.fn(async event => {
1948+
return event;
1949+
});
1950+
const processor2 = vi.fn(async () => {
1951+
throw exception;
1952+
});
1953+
const processor3 = vi.fn(event => {
1954+
return event;
1955+
});
1956+
1957+
scope.addEventProcessor(processor1);
1958+
scope.addEventProcessor(processor2);
1959+
scope.addEventProcessor(processor3);
1960+
1961+
client.captureEvent({ message: 'hello' }, {}, scope);
1962+
await vi.runOnlyPendingTimersAsync();
1963+
1964+
expect(processor1).toHaveBeenCalledTimes(1);
1965+
expect(processor2).toHaveBeenCalledTimes(1);
1966+
expect(processor3).toHaveBeenCalledTimes(0);
1967+
1968+
expect(captureExceptionSpy).toBeCalledWith(exception, {
1969+
data: {
1970+
__sentry__: true,
1971+
},
1972+
originalException: exception,
1973+
mechanism: { type: 'internal', handled: false },
1974+
});
1975+
});
1976+
18681977
test('records events dropped due to `sampleRate` option', () => {
18691978
expect.assertions(1);
18701979

0 commit comments

Comments
 (0)