-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nextjs): Add
AsyncLocalStorage
async context strategy to edge …
…SDK (#8720)
- Loading branch information
Showing
5 changed files
with
110 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import * as Sentry from '@sentry/nextjs'; | ||
|
||
export const config = { | ||
runtime: 'edge', | ||
}; | ||
|
||
export default async function handler() { | ||
// Without `runWithAsyncContext` and a working async context strategy the two spans created by `Sentry.trace()` would be nested. | ||
|
||
const outerSpanPromise = Sentry.runWithAsyncContext(() => { | ||
return Sentry.trace({ name: 'outer-span' }, () => { | ||
return new Promise<void>(resolve => setTimeout(resolve, 300)); | ||
}); | ||
}); | ||
|
||
setTimeout(() => { | ||
Sentry.runWithAsyncContext(() => { | ||
return Sentry.trace({ name: 'inner-span' }, () => { | ||
return new Promise<void>(resolve => setTimeout(resolve, 100)); | ||
}); | ||
}); | ||
}, 100); | ||
|
||
await outerSpanPromise; | ||
|
||
return new Response('ok', { status: 200 }); | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { test, expect } from '@playwright/test'; | ||
import { waitForTransaction, waitForError } from '../event-proxy-server'; | ||
|
||
test('Should allow for async context isolation in the edge SDK', async ({ request }) => { | ||
// test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); | ||
|
||
const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { | ||
return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint'; | ||
}); | ||
|
||
await request.get('/api/async-context-edge-endpoint'); | ||
|
||
const asyncContextEdgerouteTransaction = await edgerouteTransactionPromise; | ||
|
||
const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span'); | ||
const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span'); | ||
|
||
// @ts-ignore parent_span_id exists | ||
expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); | ||
// @ts-ignore parent_span_id exists | ||
expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); | ||
}); |
55 changes: 55 additions & 0 deletions
55
packages/nextjs/src/edge/asyncLocalStorageAsyncContextStrategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; | ||
import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy } from '@sentry/core'; | ||
import { GLOBAL_OBJ, logger } from '@sentry/utils'; | ||
|
||
interface AsyncLocalStorage<T> { | ||
getStore(): T | undefined; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
run<R, TArgs extends any[]>(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any | ||
const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage; | ||
|
||
/** | ||
* Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. | ||
*/ | ||
export function setAsyncLocalStorageAsyncContextStrategy(): void { | ||
if (!MaybeGlobalAsyncLocalStorage) { | ||
__DEBUG_BUILD__ && | ||
logger.warn( | ||
"Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", | ||
); | ||
return; | ||
} | ||
|
||
const asyncStorage: AsyncLocalStorage<Hub> = new MaybeGlobalAsyncLocalStorage(); | ||
|
||
function getCurrentHub(): Hub | undefined { | ||
return asyncStorage.getStore(); | ||
} | ||
|
||
function createNewHub(parent: Hub | undefined): Hub { | ||
const carrier: Carrier = {}; | ||
ensureHubOnCarrier(carrier, parent); | ||
return getHubFromCarrier(carrier); | ||
} | ||
|
||
function runWithAsyncContext<T>(callback: () => T, options: RunWithAsyncContextOptions): T { | ||
const existingHub = getCurrentHub(); | ||
|
||
if (existingHub && options?.reuseExisting) { | ||
// We're already in an async context, so we don't need to create a new one | ||
// just call the callback with the current hub | ||
return callback(); | ||
} | ||
|
||
const newHub = createNewHub(existingHub); | ||
|
||
return asyncStorage.run(newHub, () => { | ||
return callback(); | ||
}); | ||
} | ||
|
||
setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters