Skip to content

Commit e1f46b9

Browse files
committed
ref(serverless): Convert AWSServices to function
1 parent b67d9b4 commit e1f46b9

File tree

3 files changed

+121
-112
lines changed

3 files changed

+121
-112
lines changed

packages/serverless/src/awsservices.ts

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { startInactiveSpan } from '@sentry/node';
2-
import type { Integration, Span } from '@sentry/types';
1+
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
2+
import { getClient, startInactiveSpan } from '@sentry/node';
3+
import type { Client, Integration, IntegrationClass, IntegrationFn, Span } from '@sentry/types';
34
import { fill } from '@sentry/utils';
45
// 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file.
56
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here.
@@ -15,62 +16,68 @@ interface AWSService {
1516
serviceIdentifier: string;
1617
}
1718

18-
/** AWS service requests tracking */
19-
export class AWSServices implements Integration {
20-
/**
21-
* @inheritDoc
22-
*/
23-
public static id: string = 'AWSServices';
19+
const INTEGRATION_NAME = 'AWSServices';
2420

25-
/**
26-
* @inheritDoc
27-
*/
28-
public name: string;
21+
export const SETUP_CLIENTS = new WeakMap<Client, boolean>();
2922

30-
private readonly _optional: boolean;
31-
32-
public constructor(options: { optional?: boolean } = {}) {
33-
this.name = AWSServices.id;
23+
const _awsServicesIntegration = ((options: { optional?: boolean } = {}) => {
24+
const optional = options.optional || false;
25+
return {
26+
name: INTEGRATION_NAME,
27+
setupOnce() {
28+
try {
29+
// eslint-disable-next-line @typescript-eslint/no-var-requires
30+
const awsModule = require('aws-sdk/global') as typeof AWS;
31+
fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest);
32+
} catch (e) {
33+
if (!optional) {
34+
throw e;
35+
}
36+
}
37+
},
38+
setup(client) {
39+
SETUP_CLIENTS.set(client, true);
40+
},
41+
};
42+
}) satisfies IntegrationFn;
3443

35-
this._optional = options.optional || false;
36-
}
44+
export const awsServicesIntegration = defineIntegration(_awsServicesIntegration);
3745

38-
/**
39-
* @inheritDoc
40-
*/
41-
public setupOnce(): void {
42-
try {
43-
// eslint-disable-next-line @typescript-eslint/no-var-requires
44-
const awsModule = require('aws-sdk/global') as typeof AWS;
45-
fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest);
46-
} catch (e) {
47-
if (!this._optional) {
48-
throw e;
49-
}
50-
}
51-
}
52-
}
46+
/**
47+
* AWS Service Request Tracking.
48+
*
49+
* @deprecated Use `awsServicesIntegration()` instead.
50+
*/
51+
// eslint-disable-next-line deprecation/deprecation
52+
export const AWSServices = convertIntegrationFnToClass(
53+
INTEGRATION_NAME,
54+
awsServicesIntegration,
55+
) as IntegrationClass<Integration>;
5356

54-
/** */
57+
/**
58+
* Patches AWS SDK request to create `http.client` spans.
59+
*/
5560
function wrapMakeRequest<TService extends AWSService, TResult>(
5661
orig: MakeRequestFunction<GenericParams, TResult>,
5762
): MakeRequestFunction<GenericParams, TResult> {
5863
return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback<TResult>) {
5964
let span: Span | undefined;
6065
const req = orig.call(this, operation, params);
61-
req.on('afterBuild', () => {
62-
span = startInactiveSpan({
63-
name: describe(this, operation, params),
64-
op: 'http.client',
65-
origin: 'auto.http.serverless',
66+
// Only instrument if integration is active on client
67+
if (SETUP_CLIENTS.has(getClient() as Client)) {
68+
req.on('afterBuild', () => {
69+
span = startInactiveSpan({
70+
name: describe(this, operation, params),
71+
op: 'http.client',
72+
origin: 'auto.http.serverless',
73+
});
6674
});
67-
});
68-
req.on('complete', () => {
69-
if (span) {
70-
span.end();
71-
}
72-
});
73-
75+
req.on('complete', () => {
76+
if (span) {
77+
span.end();
78+
}
79+
});
80+
}
7481
if (callback) {
7582
req.send(callback);
7683
}

packages/serverless/test/__mocks__/@sentry/node.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

packages/serverless/test/awsservices.test.ts

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,72 @@ import * as SentryNode from '@sentry/node';
22
import * as AWS from 'aws-sdk';
33
import * as nock from 'nock';
44

5-
import { AWSServices } from '../src/awsservices';
5+
import { awsServicesIntegration } from '../src/awsservices';
66

7-
describe('AWSServices', () => {
8-
beforeAll(() => {
9-
new AWSServices().setupOnce();
7+
const mockSpanEnd = jest.fn();
8+
const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs }));
9+
10+
jest.mock('@sentry/node', () => {
11+
return {
12+
...jest.requireActual('@sentry/node'),
13+
startInactiveSpan: (ctx: unknown) => {
14+
mockStartInactiveSpan(ctx);
15+
return { end: mockSpanEnd };
16+
},
17+
};
18+
});
19+
20+
describe('awsServicesIntegration', () => {
21+
const mockClient = new SentryNode.NodeClient({
22+
tracesSampleRate: 1.0,
23+
integrations: [],
24+
dsn: 'https://withAWSServices@domain/123',
25+
transport: () => SentryNode.createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
26+
stackParser: () => [],
1027
});
11-
afterEach(() => {
12-
// @ts-expect-error see "Why @ts-expect-error" note
13-
SentryNode.resetMocks();
28+
29+
const integration = awsServicesIntegration();
30+
mockClient.addIntegration(integration);
31+
32+
const mockClientWithoutIntegration = new SentryNode.NodeClient({
33+
tracesSampleRate: 1.0,
34+
integrations: [],
35+
dsn: 'https://withoutAWSServices@domain/123',
36+
transport: () => SentryNode.createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
37+
stackParser: () => [],
1438
});
39+
1540
afterAll(() => {
1641
nock.restore();
1742
});
1843

44+
beforeEach(() => {
45+
SentryNode.setCurrentClient(mockClient);
46+
mockSpanEnd.mockClear();
47+
mockStartInactiveSpan.mockClear();
48+
});
49+
1950
describe('S3 tracing', () => {
2051
const s3 = new AWS.S3({ accessKeyId: '-', secretAccessKey: '-' });
2152

2253
test('getObject', async () => {
2354
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
2455
const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
2556
expect(data.Body?.toString('utf-8')).toEqual('contents');
26-
expect(SentryNode.startInactiveSpan).toBeCalledWith({
57+
expect(mockStartInactiveSpan).toBeCalledWith({
2758
op: 'http.client',
2859
origin: 'auto.http.serverless',
2960
name: 'aws.s3.getObject foo',
3061
});
31-
// @ts-expect-error see "Why @ts-expect-error" note
32-
expect(SentryNode.fakeSpan.end).toBeCalled();
62+
63+
expect(mockSpanEnd).toHaveBeenCalledTimes(1);
64+
});
65+
66+
test('getObject with integration-less client', async () => {
67+
SentryNode.setCurrentClient(mockClientWithoutIntegration);
68+
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
69+
await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
70+
expect(mockStartInactiveSpan).not.toBeCalled();
3371
});
3472

3573
test('getObject with callback', done => {
@@ -40,12 +78,22 @@ describe('AWSServices', () => {
4078
expect(data.Body?.toString('utf-8')).toEqual('contents');
4179
done();
4280
});
43-
expect(SentryNode.startInactiveSpan).toBeCalledWith({
81+
expect(mockStartInactiveSpan).toBeCalledWith({
4482
op: 'http.client',
4583
origin: 'auto.http.serverless',
4684
name: 'aws.s3.getObject foo',
4785
});
4886
});
87+
88+
test('getObject with callback with integration-less client', done => {
89+
SentryNode.setCurrentClient(mockClientWithoutIntegration);
90+
expect.assertions(1);
91+
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
92+
s3.getObject({ Bucket: 'foo', Key: 'bar' }, () => {
93+
done();
94+
});
95+
expect(mockStartInactiveSpan).not.toBeCalled();
96+
});
4997
});
5098

5199
describe('Lambda', () => {
@@ -55,11 +103,19 @@ describe('AWSServices', () => {
55103
nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply');
56104
const data = await lambda.invoke({ FunctionName: 'foo' }).promise();
57105
expect(data.Payload?.toString('utf-8')).toEqual('reply');
58-
expect(SentryNode.startInactiveSpan).toBeCalledWith({
106+
expect(mockStartInactiveSpan).toBeCalledWith({
59107
op: 'http.client',
60108
origin: 'auto.http.serverless',
61109
name: 'aws.lambda.invoke foo',
62110
});
111+
expect(mockSpanEnd).toHaveBeenCalledTimes(1);
112+
});
113+
114+
test('invoke with integration-less client', async () => {
115+
SentryNode.setCurrentClient(mockClientWithoutIntegration);
116+
nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply');
117+
await lambda.invoke({ FunctionName: 'foo' }).promise();
118+
expect(mockStartInactiveSpan).not.toBeCalled();
63119
});
64120
});
65121
});

0 commit comments

Comments
 (0)