From 15ea21727abd19932d487e0001ca0b70ad2ef876 Mon Sep 17 00:00:00 2001 From: Diederik van den Burger Date: Tue, 25 Feb 2020 21:10:05 +0100 Subject: [PATCH] feat: allow adding keys from the context to the breadcrumb (#7) --- README.md | 1 - package.json | 1 + src/Operation.ts | 18 ++++++++++++++++++ src/OperationsBreadcrumb.ts | 16 ++++++++++++++++ src/SentryLink.ts | 5 +++++ src/types.ts | 2 ++ tests/Operation.test.ts | 11 +++++++++++ tests/OperationsBreadcrumb.test.ts | 9 +++++++++ tests/SentryLink.test.ts | 17 +++++++++++++++++ tests/stubs/Operation.ts | 10 ++++++++++ 10 files changed, 89 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dca53f8..97fe9d9 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ const defaultOptions = { /** * Include context keys as extra data in the breadcrumb. Accepts dot notation. * The data is stringified and formatted. Can be used to include headers for instance. - * Note that this option is not yet implemented. */ includeContextKeys: [], }, diff --git a/package.json b/package.json index 8245640..24779b7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@sentry/types": "^5.12.4", "apollo-link": "^1.2.13", "deepmerge": "^4.2.2", + "dot-prop": "^5.2.0", "graphql": "^14.6.0" }, "devDependencies": { diff --git a/src/Operation.ts b/src/Operation.ts index f326d4c..797d246 100644 --- a/src/Operation.ts +++ b/src/Operation.ts @@ -1,4 +1,5 @@ import { Operation as ApolloOperation } from 'apollo-link'; +import dotProp from 'dot-prop'; import { ApolloLinkSentry } from './types'; import { isEmpty } from './utils'; @@ -82,4 +83,21 @@ export class Operation { ? this.operation.query?.loc.source.body : undefined ); + + /** + * Get a set of keys from the context using dot notation + * @param {string[]} keys + * @returns {{[p: string]: any} | undefined} + */ + public getContextKeys = (keys: string[]): { [s: string]: any } | undefined => { + const context = this.operation.getContext(); + + const find = keys + .map((key): object | undefined => ({ [key]: dotProp.get(context, key) })) + .reduce((a: object, b: any): object => ({ ...a, ...b }), {}); + + return !isEmpty(find) + ? find + : undefined; + }; } diff --git a/src/OperationsBreadcrumb.ts b/src/OperationsBreadcrumb.ts index e4df962..b541185 100644 --- a/src/OperationsBreadcrumb.ts +++ b/src/OperationsBreadcrumb.ts @@ -107,6 +107,22 @@ export class OperationsBreadcrumb { return this; }; + /** + * Set the breadcrumb's context + * @param {object | undefined} context + * @returns {OperationsBreadcrumb} + */ + addContext = (context: object | undefined): OperationsBreadcrumb => { + if (isEmpty(context)) return this; + + this.breadcrumb.data = { + ...this.breadcrumb.data, + context: stringifyObject(context), + }; + + return this; + }; + /** * Set the breadcrumb's response data * @param {object | undefined} response diff --git a/src/SentryLink.ts b/src/SentryLink.ts index d31832f..7dae11d 100644 --- a/src/SentryLink.ts +++ b/src/SentryLink.ts @@ -21,6 +21,7 @@ const defaultOptions: ApolloLinkSentry.Options = { includeVariables: false, includeResponse: false, includeError: false, + includeContextKeys: [], }, }; @@ -98,6 +99,10 @@ export class SentryLink extends ApolloLink { if (this.options.breadcrumb?.includeVariables) { breadcrumb.addVariables(operation.variables); } + + if (this.options?.breadcrumb?.includeContextKeys?.length) { + breadcrumb.addContext(operation.getContextKeys(this.options.breadcrumb.includeContextKeys)); + } }; /** diff --git a/src/types.ts b/src/types.ts index b226fce..cd3fc17 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,7 @@ export namespace ApolloLinkSentry { cache?: string, response?: string, error?: string, + context?: string, } } } @@ -26,6 +27,7 @@ export namespace ApolloLinkSentry { includeVariables?: boolean; includeResponse?: boolean; includeError?: boolean; + includeContextKeys?: string[]; } export interface Options { diff --git a/tests/Operation.test.ts b/tests/Operation.test.ts index f84c91b..a3cfaac 100644 --- a/tests/Operation.test.ts +++ b/tests/Operation.test.ts @@ -38,4 +38,15 @@ describe('Operation', () => { const query = OperationStub.query.loc?.source.body; expect(operation['getQuery']()).toEqual(query); }); + + it('should be possible to get context keys', () => { + const operation = new Operation(OperationStub); + const { headers } = OperationStub.getContext(); + + const keys = ['headers', 'someOtherContext.lorem.ipsum']; + const context = operation['getContextKeys'](keys); + + expect(context?.headers).toEqual(headers); + expect(context?.['someOtherContext.lorem.ipsum']).toBeTruthy(); + }); }); diff --git a/tests/OperationsBreadcrumb.test.ts b/tests/OperationsBreadcrumb.test.ts index c1382ac..68e17bd 100644 --- a/tests/OperationsBreadcrumb.test.ts +++ b/tests/OperationsBreadcrumb.test.ts @@ -65,6 +65,15 @@ describe('OperationsBreadcrumb', () => { expect(data?.variables).toBe(stringifyObject(variables)); }); + it('should be possible to set the context', () => { + const context = { headers: { 'X-Debug': true } }; + + const breadcrumb = new OperationsBreadcrumb(); + const { data } = breadcrumb.addContext(context)['breadcrumb']; + + expect(data?.context).toBe(stringifyObject(context)); + }); + it('should be possible to add a response', () => { const response = { status: 200, data: { success: true } }; diff --git a/tests/SentryLink.test.ts b/tests/SentryLink.test.ts index f0f79fb..710e60f 100644 --- a/tests/SentryLink.test.ts +++ b/tests/SentryLink.test.ts @@ -261,5 +261,22 @@ describe('SentryLink', () => { expect(originalReport.fingerprint).toBeUndefined(); }); + + test('should add context keys to the breadcrumb', () => { + const keys = ['headers', 'someOtherContext.lorem.ipsum']; + const link = new SentryLink({ breadcrumb: { includeContextKeys: keys } }); + const breadcrumb = new OperationsBreadcrumb(); + const operation = new Operation(OperationStub); + const context = operation['getContextKeys'](keys); + + link.fillBreadcrumb(breadcrumb, operation); + link.attachBreadcrumbToSentry(breadcrumb); + Sentry.captureException(new Error('Error')); + + const [report] = testkit.reports(); + const [crumb] = report.breadcrumbs; + + expect(crumb.data?.context).toBe(stringifyObject(context)); + }); }); }); diff --git a/tests/stubs/Operation.ts b/tests/stubs/Operation.ts index ee68f38..4a76a03 100644 --- a/tests/stubs/Operation.ts +++ b/tests/stubs/Operation.ts @@ -116,6 +116,16 @@ const operation: Operation = { freezeResults: false, }, }, + headers: { + 'App-Impersonation-Key': 'a0cb2511-d7b1-47a2-ad59-da06bf2f1a10', + }, + someOtherContext: { + lorem: { + ipsum: { + dorem: true, + }, + }, + }, }), setContext: (): any => {},