Skip to content

Commit 0fa1482

Browse files
committed
refactor: Extract functions and simplify SentryLink.ts
1 parent e1db20e commit 0fa1482

File tree

3 files changed

+351
-440
lines changed

3 files changed

+351
-440
lines changed

src/SentryLink.ts

Lines changed: 92 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,225 +1,113 @@
1-
import { Scope, Severity } from '@sentry/types';
2-
import deepMerge from 'deepmerge';
1+
import {
2+
ApolloLink,
3+
FetchResult,
4+
NextLink,
5+
Operation,
6+
} from '@apollo/client/core';
7+
import { Severity } from '@sentry/types';
38
import Observable from 'zen-observable';
49

5-
import { FetchResult } from '@apollo/client/link/core/types';
6-
10+
import { GraphQLBreadcrumb, makeBreadcrumb } from './breadcrumb';
11+
import { FullOptions, SentryLinkOptions, withDefaults } from './options';
712
import {
8-
ApolloLink, NextLink, Operation as ApolloOperation,
9-
} from '@apollo/client/link/core';
10-
11-
import { addBreadcrumb, configureScope } from '@sentry/minimal';
12-
import { OperationBreadcrumb } from './OperationBreadcrumb';
13-
import { Operation } from './Operation';
14-
15-
export interface Options {
16-
setTransaction?: boolean;
17-
setFingerprint?: boolean;
18-
19-
breadcrumb?: {
20-
enable?: boolean;
21-
includeQuery?: boolean;
22-
includeCache?: boolean;
23-
includeVariables?: boolean;
24-
includeResponse?: boolean;
25-
includeError?: boolean;
26-
includeContextKeys?: string[];
27-
}
28-
29-
filter?: (operation: Operation) => boolean;
30-
beforeBreadcrumb?: (breadcrumb: OperationBreadcrumb) => OperationBreadcrumb;
31-
}
32-
33-
const defaultOptions: Options = {
34-
setTransaction: true,
35-
setFingerprint: true,
36-
37-
breadcrumb: {
38-
enable: true,
39-
includeQuery: false,
40-
includeCache: false,
41-
includeVariables: false,
42-
includeResponse: false,
43-
includeError: false,
44-
includeContextKeys: [],
45-
},
46-
};
13+
attachBreadcrumbToSentry,
14+
setFingerprint,
15+
setTransaction,
16+
} from './sentry';
4717

4818
export class SentryLink extends ApolloLink {
49-
private readonly options: Options;
19+
private readonly options: FullOptions;
5020

51-
/**
52-
* Create a new ApolloLinkSentry
53-
* @param {Options} options
54-
*/
55-
constructor(options: Options = {}) {
21+
constructor(options: SentryLinkOptions = {}) {
5622
super();
57-
this.options = deepMerge(defaultOptions, options);
23+
this.options = withDefaults(options);
5824
}
5925

60-
/**
61-
* This is where the GraphQL operation is received
62-
* A breadcrumb will be created for the operation, and error/response data will be handled
63-
* @param {ApolloOperation} op
64-
* @param {NextLink} forward
65-
* @returns {Observable<FetchResult> | null}
66-
*/
67-
request = (op: ApolloOperation, forward: NextLink): Observable<FetchResult> | null => {
68-
// Obtain necessary data from the operation
69-
const operation = new Operation(op);
70-
71-
// Create a new breadcrumb for this specific operation
72-
const breadcrumb = new OperationBreadcrumb();
73-
this.fillBreadcrumb(breadcrumb, operation);
74-
75-
// Start observing the operation for results
76-
return new Observable<FetchResult>((observer) => {
77-
const subscription = forward(op).subscribe({
78-
next: (result: FetchResult) => this.handleResult(result, breadcrumb, observer),
79-
complete: () => this.handleComplete(breadcrumb, observer),
80-
error: (error: any) => this.handleError(breadcrumb, error, observer),
81-
});
82-
83-
// Close the subscription
84-
return () => {
85-
if (subscription) subscription.unsubscribe();
86-
};
87-
});
88-
};
89-
90-
/**
91-
* Fill the breadcrumb with information, respecting the provided options
92-
* The breadcrumb is not yet attached to Sentry after this method
93-
* @param {OperationBreadcrumb} breadcrumb
94-
* @param {Operation} operation
95-
*/
96-
fillBreadcrumb = (breadcrumb: OperationBreadcrumb, operation: Operation): void => {
97-
// Apply the filter option
98-
if (typeof this.options.filter === 'function') {
99-
const stop = breadcrumb.filter(this.options.filter(operation));
100-
if (stop) return;
26+
request(
27+
operation: Operation,
28+
forward: NextLink,
29+
): Observable<FetchResult> | null {
30+
if (typeof this.options.shouldHandleOperation === 'function') {
31+
if (!this.options.shouldHandleOperation(operation)) {
32+
return forward(operation);
33+
}
10134
}
10235

103-
breadcrumb
104-
.setMessage(operation.name)
105-
.setCategory(operation.type);
106-
107-
// TODO: Maybe move this to a different place? It isn't a breadcrumb
10836
if (this.options.setTransaction) {
109-
this.setTransaction(operation);
37+
setTransaction(operation);
11038
}
11139

112-
// TODO: Maybe move this to a different place? It isn't a breadcrumb
11340
if (this.options.setFingerprint) {
114-
this.setFingerprint();
115-
}
116-
117-
if (this.options.breadcrumb?.includeQuery) {
118-
breadcrumb.setQuery(operation.query);
119-
}
120-
121-
if (this.options.breadcrumb?.includeCache) {
122-
breadcrumb.setCache(operation.cache);
41+
setFingerprint(operation);
12342
}
12443

125-
if (this.options.breadcrumb?.includeVariables) {
126-
breadcrumb.setVariables(operation.variables);
127-
}
128-
129-
if (this.options?.breadcrumb?.includeContextKeys?.length) {
130-
breadcrumb.setContext(operation.getContextKeys(this.options.breadcrumb.includeContextKeys));
131-
}
132-
};
133-
134-
/**
135-
* Handle the operation's response
136-
* The breadcrumb is not yet attached to Sentry after this method
137-
* @param {FetchResult} result
138-
* @param {OperationBreadcrumb} breadcrumb
139-
* @param observer
140-
*/
141-
handleResult = (result: FetchResult, breadcrumb: OperationBreadcrumb, observer: any): void => {
142-
if (this.options.breadcrumb?.includeResponse) {
143-
breadcrumb.setResponse(result);
144-
}
145-
146-
observer.next(result);
147-
};
148-
149-
/**
150-
* Changes the level and type of the breadcrumb to `error`
151-
* Furthermore, if the includeError option is truthy, the error data will be attached
152-
* Then, the error will be attached to Sentry
153-
* @param {OperationBreadcrumb} breadcrumb
154-
* @param error
155-
* @param observer
156-
*/
157-
handleError = (breadcrumb: OperationBreadcrumb, error: any, observer: any): void => {
158-
breadcrumb
159-
.setLevel(Severity.Error)
160-
.setType('error');
161-
162-
if (this.options.breadcrumb?.includeError) {
163-
breadcrumb.setError(error);
164-
}
44+
const breadcrumb = this.options.attachBreadcrumbs
45+
? makeBreadcrumb(operation, this.options)
46+
: undefined;
47+
48+
// While this could be done more simplistically by simply subscribing,
49+
// wrapping the observer in our own observer ensures we get the results
50+
// before they are passed along to other observers. This guarantees we
51+
// get to run our instrumentation before others observers potentially
52+
// throw and thus flush the results to Sentry.
53+
return new Observable<FetchResult>((originalObserver) => {
54+
const subscription = forward(operation).subscribe({
55+
next: (result) => {
56+
if (this.options.attachBreadcrumbs) {
57+
// We must have a breadcrumb if attachBreadcrumbs was set
58+
(breadcrumb as GraphQLBreadcrumb).level = severityForResult(result);
59+
60+
if (this.options.attachBreadcrumbs.includeFetchResult) {
61+
// We must have a breadcrumb if attachBreadcrumbs was set
62+
(breadcrumb as GraphQLBreadcrumb).data.fetchResult = result;
63+
}
64+
}
65+
66+
originalObserver.next(result);
67+
},
68+
complete: () => {
69+
if (this.options.attachBreadcrumbs) {
70+
attachBreadcrumbToSentry(
71+
operation,
72+
// We must have a breadcrumb if attachBreadcrumbs was set
73+
breadcrumb as GraphQLBreadcrumb,
74+
this.options,
75+
);
76+
}
77+
78+
originalObserver.complete();
79+
},
80+
error: (error) => {
81+
if (this.options.attachBreadcrumbs) {
82+
// We must have a breadcrumb if attachBreadcrumbs was set
83+
(breadcrumb as GraphQLBreadcrumb).level = Severity.Error;
84+
85+
if (this.options.attachBreadcrumbs.includeError) {
86+
// We must have a breadcrumb if attachBreadcrumbs was set
87+
(breadcrumb as GraphQLBreadcrumb).data.error = error;
88+
}
89+
90+
attachBreadcrumbToSentry(
91+
operation,
92+
// We must have a breadcrumb if attachBreadcrumbs was set
93+
breadcrumb as GraphQLBreadcrumb,
94+
this.options,
95+
);
96+
}
97+
98+
originalObserver.error(error);
99+
},
100+
});
165101

166-
this.attachBreadcrumbToSentry(breadcrumb);
167-
168-
observer.error(error);
169-
};
170-
171-
/**
172-
* Since no error occurred, it is time to attach the breadcrumb to Sentry
173-
* @param {OperationBreadcrumb} breadcrumb
174-
* @param observer
175-
*/
176-
handleComplete = (breadcrumb: OperationBreadcrumb, observer: any): void => {
177-
this.attachBreadcrumbToSentry(breadcrumb);
178-
observer.complete();
179-
};
180-
181-
/**
182-
* Set the Sentry transaction
183-
* @param {Operation} operation
184-
*/
185-
setTransaction = (operation: Operation): void => {
186-
configureScope((scope: Scope) => {
187-
scope.setTransactionName(operation.name);
188-
});
189-
};
190-
191-
/**
192-
* Set the Sentry fingerprint
193-
*/
194-
setFingerprint = (): void => {
195-
configureScope((scope: Scope) => {
196-
scope.setFingerprint([
197-
'{{default}}',
198-
'{{transaction}}',
199-
]);
102+
return () => {
103+
subscription.unsubscribe();
104+
};
200105
});
201-
};
202-
203-
/**
204-
* Attach the breadcrumb to the Sentry event
205-
* @param {OperationBreadcrumb} breadcrumb
206-
*/
207-
attachBreadcrumbToSentry = (breadcrumb: OperationBreadcrumb): void => {
208-
// Apply options
209-
if (this.options.breadcrumb?.enable === false) return;
210-
if (breadcrumb.filtered) return;
211-
212-
if (breadcrumb.flushed) {
213-
console.warn('[apollo-link-sentry] SentryLink.attachBreadcrumbToSentry() was called on an already flushed breadcrumb');
214-
return;
215-
}
216-
217-
if (typeof this.options.beforeBreadcrumb === 'function') {
218-
const after = this.options.beforeBreadcrumb(breadcrumb);
219-
addBreadcrumb(after.flush());
220-
return;
221-
}
106+
}
107+
}
222108

223-
addBreadcrumb(breadcrumb.flush());
224-
};
109+
function severityForResult(result: FetchResult): Severity {
110+
return result.errors && result.errors.length > 0
111+
? Severity.Error
112+
: Severity.Info;
225113
}

src/sentry.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Operation } from '@apollo/client/core';
2+
import { addBreadcrumb, configureScope } from '@sentry/minimal';
3+
import { Scope } from '@sentry/types';
4+
5+
import { GraphQLBreadcrumb } from './breadcrumb';
6+
import { extractDefinition } from './operation';
7+
import { FullOptions } from './options';
8+
9+
export function setTransaction(operation: Operation): void {
10+
const definition = extractDefinition(operation);
11+
const name = definition.name;
12+
13+
if (name) {
14+
configureScope((scope: Scope) => {
15+
scope.setTransactionName(name.value);
16+
});
17+
}
18+
}
19+
20+
export const DEFAULT_FINGERPRINT = '{{ default }}';
21+
22+
export function setFingerprint(operation: Operation): void {
23+
const definition = extractDefinition(operation);
24+
const name = definition.name;
25+
26+
if (name) {
27+
configureScope((scope: Scope) => {
28+
scope.setFingerprint([DEFAULT_FINGERPRINT, name.value]);
29+
});
30+
}
31+
}
32+
33+
export function attachBreadcrumbToSentry(
34+
operation: Operation,
35+
breadcrumb: GraphQLBreadcrumb,
36+
options: FullOptions,
37+
): void {
38+
if (
39+
options.attachBreadcrumbs &&
40+
typeof options.attachBreadcrumbs.transform === 'function'
41+
) {
42+
addBreadcrumb(options.attachBreadcrumbs.transform(breadcrumb, operation));
43+
return;
44+
}
45+
46+
addBreadcrumb(breadcrumb);
47+
}

0 commit comments

Comments
 (0)