Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(node): Fix domain scope inheritance #7799

Merged
merged 23 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eea031e
feat(node): Migrate to domains use `AsyncContextStrategy`
timfish Apr 6, 2023
2d18818
Pass hub
timfish Apr 6, 2023
6672552
rename
timfish Apr 6, 2023
2097e85
Merge remote-tracking branch 'upstream/develop' into feat/migrate-asy…
timfish Apr 6, 2023
8f9a8dd
Update to use options object
timfish Apr 6, 2023
7029e82
Late in the day linting
timfish Apr 6, 2023
f78af70
Merge branch 'develop' into feat/migrate-async-strategy
AbhiPrasad Apr 7, 2023
0142be4
Serverless
timfish Apr 7, 2023
db0e5a2
Merge branch 'feat/migrate-async-strategy' of https://github.com/timf…
timfish Apr 7, 2023
1072ddc
Merge branch 'develop' into feat/migrate-async-strategy
timfish Apr 7, 2023
b600442
Remove domainify
timfish Apr 7, 2023
1ee193b
Merge branch 'feat/migrate-async-strategy' of https://github.com/timf…
timfish Apr 7, 2023
6cb7c1a
Rename `args` to `emitters`
timfish Apr 7, 2023
8e59ea1
One more domainify
timfish Apr 7, 2023
86ebca1
Remove redundant comments!
timfish Apr 7, 2023
c47468f
Re-export `runWithAsyncContext` from `@sentry/node`
timfish Apr 7, 2023
b7688f8
Revert "Remove domainify"
timfish Apr 7, 2023
ffcdd6e
Merge branch 'develop' into feat/migrate-async-strategy
timfish Apr 7, 2023
0380ab4
Fix domains to use proper scope inheritance
timfish Apr 9, 2023
0da5681
Fix handler tests
timfish Apr 9, 2023
f4e97de
Merge branch 'develop' into fix/domain-scope-inheritance
timfish Apr 11, 2023
b064621
Lint
timfish Apr 11, 2023
9806abf
Merge branch 'develop' into fix/domain-scope-inheritance
timfish Apr 11, 2023
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
4 changes: 2 additions & 2 deletions packages/core/src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,10 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub {
*
* If the carrier does not contain a hub, a new hub is created with the global hub client and scope.
*/
export function ensureHubOnCarrier(carrier: Carrier): void {
export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void {
// If there's no hub on current domain, or it's an old API, assign a new one
if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) {
const globalHubTopStack = getGlobalHub().getStackTop();
const globalHubTopStack = parent.getStackTop();
setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope)));
}
}
Expand Down
40 changes: 21 additions & 19 deletions packages/node/src/async/domain.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core';
import {
ensureHubOnCarrier,
getCurrentHub as getCurrentHubCore,
getHubFromCarrier,
setAsyncContextStrategy,
} from '@sentry/core';
import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy, setHubOnCarrier } from '@sentry/core';
import * as domain from 'domain';
import { EventEmitter } from 'events';

Expand All @@ -26,33 +21,40 @@ function getCurrentHub(): Hub | undefined {
return getHubFromCarrier(activeDomain);
}

function createNewHub(parent: Hub | undefined): Hub {
const carrier: Carrier = {};
ensureHubOnCarrier(carrier, parent);
return getHubFromCarrier(carrier);
}

function runWithAsyncContext<T>(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T {
if (options?.reuseExisting) {
const activeDomain = getActiveDomain<domain.Domain & Carrier>();
const activeDomain = getActiveDomain<domain.Domain & Carrier>();

if (activeDomain) {
for (const emitter of options.emitters || []) {
if (emitter instanceof EventEmitter) {
activeDomain.add(emitter);
}
if (activeDomain && options?.reuseExisting) {
for (const emitter of options.emitters || []) {
if (emitter instanceof EventEmitter) {
activeDomain.add(emitter);
}

// We're already in a domain, so we don't need to create a new one, just call the callback with the current hub
return callback(getHubFromCarrier(activeDomain));
}

// We're already in a domain, so we don't need to create a new one, just call the callback with the current hub
return callback(getHubFromCarrier(activeDomain));
}

const local = domain.create();
const local = domain.create() as domain.Domain & Carrier;

for (const emitter of options.emitters || []) {
if (emitter instanceof EventEmitter) {
local.add(emitter);
}
}

const parentHub = activeDomain ? getHubFromCarrier(activeDomain) : undefined;
const newHub = createNewHub(parentHub);
setHubOnCarrier(local, newHub);

return local.bind(() => {
const hub = getCurrentHubCore();
return callback(hub);
return callback(newHub);
})();
}

Expand Down
34 changes: 22 additions & 12 deletions packages/node/test/async/domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,37 @@ describe('domains', () => {
expect(hub).toEqual(new Hub());
});

test('domain hub scope inheritance', () => {
test('hub scope inheritance', () => {
setDomainAsyncContextStrategy();

const globalHub = getCurrentHub();
globalHub.configureScope(scope => {
scope.setExtra('a', 'b');
scope.setTag('a', 'b');
scope.addBreadcrumb({ message: 'a' });
});
runWithAsyncContext(hub => {
expect(globalHub).toEqual(hub);
globalHub.setExtra('a', 'b');

runWithAsyncContext(hub1 => {
expect(hub1).toEqual(globalHub);

hub1.setExtra('c', 'd');
expect(hub1).not.toEqual(globalHub);

runWithAsyncContext(hub2 => {
expect(hub2).toEqual(hub1);
expect(hub2).not.toEqual(globalHub);

hub2.setExtra('e', 'f');
expect(hub2).not.toEqual(hub1);
});
});
});

test('domain hub single instance', () => {
test('hub single instance', () => {
setDomainAsyncContextStrategy();

runWithAsyncContext(hub => {
expect(hub).toBe(getCurrentHub());
});
});

test('domain within a domain not reused', () => {
test('within a domain not reused', () => {
setDomainAsyncContextStrategy();

runWithAsyncContext(hub1 => {
Expand All @@ -46,7 +56,7 @@ describe('domains', () => {
});
});

test('domain within a domain reused when requested', () => {
test('within a domain reused when requested', () => {
setDomainAsyncContextStrategy();

runWithAsyncContext(hub1 => {
Expand All @@ -59,7 +69,7 @@ describe('domains', () => {
});
});

test('concurrent domain hubs', done => {
test('concurrent hub contexts', done => {
setDomainAsyncContextStrategy();

let d1done = false;
Expand Down
36 changes: 30 additions & 6 deletions packages/node/test/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import type { Hub } from '@sentry/core';
import * as sentryCore from '@sentry/core';
import { Transaction } from '@sentry/core';
import { setAsyncContextStrategy, Transaction } from '@sentry/core';
import type { Event } from '@sentry/types';
import { SentryError } from '@sentry/utils';
import * as http from 'http';

import { setDomainAsyncContextStrategy } from '../src/async/domain';
import { NodeClient } from '../src/client';
import { errorHandler, requestHandler, tracingHandler } from '../src/handlers';
import * as SDK from '../src/sdk';
import { getDefaultNodeClientOptions } from './helper/node-client-options';

setDomainAsyncContextStrategy();
function mockAsyncContextStrategy(getHub: () => Hub): void {
function getCurrentHub(): Hub | undefined {
return getHub();
}

function runWithAsyncContext<T>(fn: (hub: Hub) => T): T {
return fn(getHub());
}

setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext });
}

describe('requestHandler', () => {
const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' };
Expand Down Expand Up @@ -52,6 +62,7 @@ describe('requestHandler', () => {
const hub = new sentryCore.Hub(client);

jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

sentryRequestMiddleware(req, res, next);

Expand All @@ -65,6 +76,7 @@ describe('requestHandler', () => {
const hub = new sentryCore.Hub(client);

jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

sentryRequestMiddleware(req, res, next);

Expand All @@ -78,6 +90,7 @@ describe('requestHandler', () => {
const hub = new sentryCore.Hub(client);

jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

const captureRequestSession = jest.spyOn<any, any>(client, '_captureRequestSession');

Expand All @@ -97,7 +110,9 @@ describe('requestHandler', () => {
const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' });
client = new NodeClient(options);
const hub = new sentryCore.Hub(client);

jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

const captureRequestSession = jest.spyOn<any, any>(client, '_captureRequestSession');

Expand Down Expand Up @@ -142,6 +157,7 @@ describe('requestHandler', () => {
it('stores request and request data options in `sdkProcessingMetadata`', () => {
const hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions()));
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

const requestHandlerOptions = { include: { ip: false } };
const sentryRequestMiddleware = requestHandler(requestHandlerOptions);
Expand Down Expand Up @@ -177,6 +193,7 @@ describe('tracingHandler', () => {
beforeEach(() => {
hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions({ tracesSampleRate: 1.0 })));
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);
req = {
headers,
method,
Expand Down Expand Up @@ -274,6 +291,8 @@ describe('tracingHandler', () => {
const tracesSampler = jest.fn();
const options = getDefaultNodeClientOptions({ tracesSampler });
const hub = new sentryCore.Hub(new NodeClient(options));
mockAsyncContextStrategy(() => hub);

hub.run(() => {
sentryTracingMiddleware(req, res, next);

Expand All @@ -296,6 +315,7 @@ describe('tracingHandler', () => {
const hub = new sentryCore.Hub(new NodeClient(options));

jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
mockAsyncContextStrategy(() => hub);

sentryTracingMiddleware(req, res, next);

Expand Down Expand Up @@ -502,14 +522,17 @@ describe('errorHandler()', () => {
client.initSessionFlusher();
const scope = new sentryCore.Scope();
const hub = new sentryCore.Hub(client, scope);
mockAsyncContextStrategy(() => hub);

jest.spyOn<any, any>(client, '_captureRequestSession');

hub.run(() => {
scope?.setRequestSession({ status: 'ok' });
sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next);
const requestSession = scope?.getRequestSession();
expect(requestSession).toEqual({ status: 'crashed' });
sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => {
const scope = sentryCore.getCurrentHub().getScope();
const requestSession = scope?.getRequestSession();
expect(requestSession).toEqual({ status: 'crashed' });
});
});
});

Expand All @@ -535,6 +558,7 @@ describe('errorHandler()', () => {
client = new NodeClient(options);

const hub = new sentryCore.Hub(client);
mockAsyncContextStrategy(() => hub);
sentryCore.makeMain(hub);

// `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch
Expand Down