8000 Add AWSResources integration · rchl/sentry-javascript@af71624 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit af71624

Browse files
marshall-leekamilogorek
authored andcommitted
Add AWSResources integration
This integration traces AWS service calls as spans.
1 parent b4a29be commit af71624

File tree

8 files changed

+293
-5
lines changed

8 files changed

+293
-5
lines changed

packages/serverless/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ Currently supported environment:
2121

2222
*AWS Lambda*
2323

24-
To use this SDK, call `Sentry.init(options)` at the very beginning of your JavaScript file.
24+
To use this SDK, call `Sentry.AWSLambda.init(options)` at the very beginning of your JavaScript file.
2525

2626
```javascript
2727
import * as Sentry from '@sentry/serverless';
2828

29-
Sentry.init({
29+
Sentry.AWSLambda.init({
3030
dsn: '__DSN__',
3131
// ...
3232
});
@@ -41,3 +41,14 @@ exports.handler = Sentry.AWSLambda.wrapHandler((event, context, callback) => {
4141
throw new Error('oh, hello there!');
4242
});
4343
```
44+
45+
If you also want to trace performance of all the incoming requests and also outgoing AWS service requests, just set the `tracesSampleRate` option.
46+
47+
```javascript
48+
import * as Sentry from '@sentry/serverless';
49+
50+
Sentry.AWSLambda.init({
51+
dsn: '__DSN__',
52+
tracesSampleRate: 1.0,
53+
});
54+
```

packages/serverless/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
"@sentry-internal/eslint-config-sdk": "5.25.0",
2828
"@types/aws-lambda": "^8.10.62",
2929
"@types/node": "^14.6.4",
30+
"aws-sdk": "^2.765.0",
3031
"eslint": "7.6.0",
3132
"jest": "^24.7.1",
33+
"nock": "^13.0.4",
3234
"npm-run-all": "^4.1.2",
3335
"prettier": "1.19.0",
3436
"rimraf": "^2.6.3",

packages/serverless/src/awslambda.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
startTransaction,
1010
withScope,
1111
} from '@sentry/node';
12+
import * as Sentry from '@sentry/node';
13+
import { Integration } from '@sentry/types';
1214
import { addExceptionMechanism } from '@sentry/utils';
1315
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
1416
// eslint-disable-next-line import/no-unresolved
@@ -17,6 +19,10 @@ import { hostname } from 'os';
1719
import { performance } from 'perf_hooks';
1820
import { types } from 'util';
1921

22+
import { AWSServices } from './awsservices';
23+
24+
export * from '@sentry/node';
25+
2026
const { isPromise } = types;
2127

2228
// https://www.npmjs.com/package/aws-lambda-consumer
@@ -39,6 +45,18 @@ export interface WrapperOptions {
3945
timeoutWarningLimit: number;
4046
}
4147

48+
export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices()];
49+
50+
/**
51+
* @see {@link Sentry.init}
52+
*/
53+
export function init(options: Sentry.NodeOptions = {}): void {
54+
if (options.defaultIntegrations === undefined) {
55+
options.defaultIntegrations = defaultIntegrations;
56+
}
57+
return Sentry.init(options);
58+
}
59+
4260
/**
4361
* Add event processor that will override SDK details to point to the serverless SDK instead of Node,
4462
* as well as set correct mechanism type, which should be set to `handled: false`.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { getCurrentHub } from '@sentry/node';
2+
import { Integration, Span, Transaction } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
// 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file.
5+
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here.
6+
import * as AWS from 'aws-sdk/global';
7+
8+
type GenericParams = { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
9+
type MakeRequestCallback<TResult> = (err: AWS.AWSError, data: TResult) => void;
10+
// This interace could be replaced with just type alias once the `strictBindCallApply` mode is enabled.
11+
interface MakeRequestFunction<TParams, TResult> extends CallableFunction {
12+
(operation: string, params?: TParams, callback?: MakeRequestCallback<TResult>): AWS.Request<TResult, AWS.AWSError>;
13+
}
14+
interface AWSService {
15+
serviceIdentifier: string;
16+
}
17+
18+
/** AWS service requests tracking */
19+
export class AWSServices implements Integration {
20+
/**
21+
* @inheritDoc
22+
*/
23+
public static id: string = 'AWSServices';
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public name: string = AWSServices.id;
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public setupOnce(): void {
34+
const awsModule = require('aws-sdk/global') as typeof AWS;
35+
fill(
36+
awsModule.Service.prototype,
37+
'makeRequest',
38+
<TService extends AWSService, TResult>(
39+
orig: MakeRequestFunction<GenericParams, TResult>,
40+
): MakeRequestFunction<GenericParams, TResult> =>
41+
function(this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback<TResult>) {
42+
let transaction: Transaction | undefined;
43+
let span: Span | undefined;
44+
const scope = getCurrentHub().getScope();
45+
if (scope) {
46+
transaction = scope.getTransaction();
47+
}
48+
const req = orig.call(this, operation, params);
49+
req.on('afterBuild', () => {
50+
if (transaction) {
51+
span = transaction.startChild({
52+
description: describe(this, operation, params),
53+
op: 'request',
54+
});
55+
}
56+
});
57+
req.on('complete', () => {
58+
if (span) {
59+
span.finish();
60+
}
61+
});
62+
63+
if (callback) {
64+
req.send(callback);
65+
}
66+
return req;
67+
},
68+
);
69+
}
70+
}
71+
72+
/** Describes an operation on generic AWS service */
73+
function describe<TService extends AWSService>(service: TService, operation: string, params?: GenericParams): string {
74+
let ret = `aws.${service.serviceIdentifier}.${operation}`;
75+
if (params === undefined) {
76+
return ret;
77+
}
78+
switch (service.serviceIdentifier) {
79+
case 's3':
80+
ret += describeS3Operation(operation, params);
81+
break;
82+
case 'lambda':
83+
ret += describeLambdaOperation(operation, params);
84+
break;
85+
}
86+
return ret;
87+
}
88+
89+
/** Describes an operation on AWS Lambda service */
90+
function describeLambdaOperation(_operation: string, params: GenericParams): string {
91+
let ret = '';
92+
if ('FunctionName' in params) {
93+
ret += ` ${params.FunctionName}`;
94+
}
95+
return ret;
96+
}
97+
98+
/** Describes an operation on AWS S3 service */
99+
function describeS3Operation(_operation: string, params: GenericParams): string {
100+
let ret = '';
101+
if ('Bucket' in params) {
102+
ret += ` ${params.Bucket}`;
103+
}
104+
return ret;
105+
}

packages/serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import * as AWSLambda from './awslambda';
33
import * as GCPFunction from './gcpfunction';
44
export { AWSLambda, GCPFunction };
55

6+
export * from './awsservices';
67
export * from '@sentry/node';

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
const origSentry = jest.requireActual('@sentry/node');
2+
export const defaultIntegrations = origSentry.defaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
23
export const Handlers = origSentry.Handlers; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
34
export const SDK_VERSION = '6.6.6';
45
export const Severity = {
56
Warning: 'warning',
67
};
78
export const fakeParentScope = {
89
setSpan: jest.fn(),
10+
getTransaction: jest.fn(() => fakeTransaction),
911
};
1012
export const fakeHub = {
1113
configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeParentScope)),
14+
getScope: jest.fn(() => fakeParentScope),
1215
};
1316
export const fakeScope = {
1417
addEventProcessor: jest.fn(),
1518
setTransactionName: jest.fn(),
1619
setTag: jest.fn(),
1720
setContext: jest.fn(),
1821
};
22+
export const fakeSpan = {
23+
finish: jest.fn(),
24+
};
1925
export const fakeTransaction = {
2026
finish: jest.fn(),
2127
setHttpStatus: jest.fn(),
28+
startChild: jest.fn(() => fakeSpan),
2229
};
2330
export const getCurrentHub = jest.fn(() => fakeHub);
2431
export const startTransaction = jest.fn(_ => fakeTransaction);
@@ -30,8 +37,12 @@ export const flush = jest.fn(() => Promise.resolve());
3037
export const resetMocks = (): void => {
3138
fakeTransaction.setHttpStatus.mockClear();
3239
fakeTransaction.finish.mockClear();
40+
fakeTransaction.startChild.mockClear();
41+
fakeSpan.finish.mockClear();
3342
fakeParentScope.setSpan.mockClear();
43+
fakeParentScope.getTransaction.mockClear();
3444
fakeHub.configureScope.mockClear();
45+
fakeHub.getScope.mockClear();
3546

3647
fakeScope.addEventProcessor.mockClear();
3748
fakeScope.setTransactionName.mockClear();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as AWS from 'aws-sdk';
2+
import * as nock from 'nock';
3+
4+
import * as Sentry from '../src';
5+
import { AWSServices } from '../src/awsservices';
6+
7+
/**
8+
* Why @ts-ignore some Sentry.X calls
9+
*
10+
* A hack-ish way to contain everything related to mocks in the same __mocks__ file.
11+
* Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it.
12+
*/
13+
14+
describe('AWSServices', () => {
15+
beforeAll(() => {
16+
new AWSServices().setupOnce();
17+
});
18+
afterEach(() => {
19+
// @ts-ignore see "Why @ts-ignore" note
20+
Sentry.resetMocks();
21+
});
22+
afterAll(() => {
23+
nock.restore();
24+
});
25+
26+
describe('S3', () => {
27+
const s3 = new AWS.S3({ accessKeyId: '-', secretAccessKey: '-' });
28+
29+
test('getObject', async () => {
30+
nock('https://foo.s3.amazonaws.com')
31+
.get('/bar')
32+
.reply(200, 'contents');
33+
const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
34+
expect(data.Body?.toString('utf-8')).toEqual('contents');
35+
// @ts-ignore see "Why @ts-ignore" note
36+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.s3.getObject foo' });
37+
// @ts-ignore see "Why @ts-ignore" note
38+
expect(Sentry.fakeSpan.finish).toBeCalled();
39+
});
40+
41+
test('getObject with callback', done => {
42+
expect.assertions(3);
43+
nock('https://foo.s3.amazonaws.com')
44+
.get('/bar')
45+
.reply(200, 'contents');
46+
s3.getObject({ Bucket: 'foo', Key: 'bar' }, (err, data) => {
47+
expect(err).toBeNull();
48+
expect(data.Body?.toString('utf-8')).toEqual('contents');
49+
done();
50+
});
51+
// @ts-ignore see "Why @ts-ignore" note
52+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.s3.getObject foo' });
53+
});
54+
});
55+
56+
describe('Lambda', () => {
57+
const lambda = new AWS.Lambda({ accessKeyId: '-', secretAccessKey: '-', region: 'eu-north-1' });
58+
59+
test('invoke', async () => {
60+
nock('https://lambda.eu-north-1.amazonaws.com')
61+
.post('/2015-03-31/functions/foo/invocations')
62+
.reply(201, 'reply');
63+
const data = await lambda.invoke({ FunctionName: 'foo' }).promise();
64+
expect(data.Payload?.toString('utf-8')).toEqual('reply');
65+
// @ts-ignore see "Why @ts-ignore" note
66+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.lambda.invoke foo' });
67+
});
68+
});
69+
});

0 commit comments

Comments
 (0)
0