Skip to content

Commit 36492ce

Browse files
mydeaLms24
andauthored
ref: Avoid optional chaining & add eslint rule (#6777)
As this is transpiled to a rather verbose form. Co-authored-by: Lukas Stracke <[email protected]>
1 parent 0122a9f commit 36492ce

27 files changed

+162
-54
lines changed

packages/browser/src/client.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
9696
const breadcrumbIntegration = this.getIntegrationById(BREADCRUMB_INTEGRATION_ID) as Breadcrumbs | undefined;
9797
// We check for definedness of `addSentryBreadcrumb` in case users provided their own integration with id
9898
// "Breadcrumbs" that does not have this function.
99-
breadcrumbIntegration?.addSentryBreadcrumb?.(event);
99+
if (breadcrumbIntegration && breadcrumbIntegration.addSentryBreadcrumb) {
100+
breadcrumbIntegration.addSentryBreadcrumb(event);
101+
}
100102

101103
super.sendEvent(event, hint);
102104
}

packages/eslint-config-sdk/src/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ module.exports = {
143143
},
144144
],
145145

146-
// We want to prevent async await usage in our files to prevent uncessary bundle size. Turned off in tests.
146+
// We want to prevent async await & optional chaining usage in our files to prevent uncessary bundle size. Turned off in tests.
147147
'@sentry-internal/sdk/no-async-await': 'error',
148+
'@sentry-internal/sdk/no-optional-chaining': 'error',
148149

149150
// JSDOC comments are required for classes and methods. As we have a public facing codebase, documentation,
150151
// even if it may seems excessive at times, is important to emphasize. Turned off in tests.
@@ -178,6 +179,7 @@ module.exports = {
178179
'@typescript-eslint/no-non-null-assertion': 'off',
179180
'@typescript-eslint/no-empty-function': 'off',
180181
'@sentry-internal/sdk/no-async-await': 'off',
182+
'@sentry-internal/sdk/no-optional-chaining': 'off',
181183
},
182184
},
183185
{

packages/eslint-plugin-sdk/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
module.exports = {
1212
rules: {
1313
'no-async-await': require('./rules/no-async-await'),
14+
'no-optional-chaining': require('./rules/no-optional-chaining'),
1415
'no-eq-empty': require('./rules/no-eq-empty'),
1516
},
1617
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @fileoverview Rule to disallow using optional chaining - because this is transpiled into verbose code.
3+
* @author Francesco Novy
4+
*
5+
* Based on https://github.com/facebook/lexical/pull/3233
6+
*/
7+
'use strict';
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
/** @type {import('eslint').Rule.RuleModule} */
14+
module.exports = {
15+
meta: {
16+
type: 'problem',
17+
docs: {
18+
description: 'disallow usage of optional chaining',
19+
category: 'Best Practices',
20+
recommended: true,
21+
},
22+
messages: {
23+
forbidden: 'Avoid using optional chaining',
24+
},
25+
fixable: null,
26+
schema: [],
27+
},
28+
29+
create(context) {
30+
const sourceCode = context.getSourceCode();
31+
32+
/**
33+
* Checks if the given token is a `?.` token or not.
34+
* @param {Token} token The token to check.
35+
* @returns {boolean} `true` if the token is a `?.` token.
36+
*/
37+
function isQuestionDotToken(token) {
38+
return (
39+
token.value === '?.' &&
40+
(token.type === 'Punctuator' || // espree has been parsed well.
41+
// [email protected] doesn't parse "?." tokens well. Therefore, get the string from the source code and check it.
42+
sourceCode.getText(token) === '?.')
43+
);
44+
}
45+
46+
return {
47+
'CallExpression[optional=true]'(node) {
48+
context.report({
49+
messageId: 'forbidden',
50+
node: sourceCode.getTokenAfter(node.callee, isQuestionDotToken),
51+
});
52+
},
53+
'MemberExpression[optional=true]'(node) {
54+
context.report({
55+
messageId: 'forbidden',
56+
node: sourceCode.getTokenAfter(node.object, isQuestionDotToken),
57+
});
58+
},
59+
};
60+
},
61+
};

packages/nextjs/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
extends: ['../../.eslintrc.js'],
1111
rules: {
1212
'@sentry-internal/sdk/no-async-await': 'off',
13+
'@sentry-internal/sdk/no-optional-chaining': 'off',
1314
},
1415
overrides: [
1516
{

packages/node/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ module.exports = {
55
extends: ['../../.eslintrc.js'],
66
rules: {
77
'@sentry-internal/sdk/no-async-await': 'off',
8+
'@sentry-internal/sdk/no-optional-chaining': 'off',
89
},
910
};

packages/opentelemetry-node/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ module.exports = {
55
extends: ['../../.eslintrc.js'],
66
rules: {
77
'@sentry-internal/sdk/no-async-await': 'off',
8+
'@sentry-internal/sdk/no-optional-chaining': 'off',
89
},
910
};

packages/react/src/reactrouterv6.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes {
226226

227227
// A value with stable identity to either pick `locationArg` if available or `location` if not
228228
const stableLocationParam =
229-
typeof locationArg === 'string' || locationArg?.pathname !== undefined
229+
typeof locationArg === 'string' || (locationArg && locationArg.pathname)
230230
? (locationArg as { pathname: string })
231231
: location;
232232

packages/remix/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
extends: ['../../.eslintrc.js'],
1111
rules: {
1212
'@sentry-internal/sdk/no-async-await': 'off',
13+
'@sentry-internal/sdk/no-optional-chaining': 'off',
1314
},
1415
overrides: [
1516
{

packages/replay/src/coreHandlers/handleGlobalEvent.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even
2121

2222
// Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb
2323
// As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users
24-
if (isRrwebError(event) && !replay.getOptions()._experiments?.captureExceptions) {
24+
if (isRrwebError(event) && !replay.getOptions()._experiments.captureExceptions) {
2525
__DEBUG_BUILD__ && logger.log('[Replay] Ignoring error from rrweb internals', event);
2626
return null;
2727
}
2828

2929
// Only tag transactions with replayId if not waiting for an error
3030
// @ts-ignore private
3131
if (!event.type || replay.recordingMode === 'session') {
32-
event.tags = { ...event.tags, replayId: replay.session?.id };
32+
event.tags = { ...event.tags, replayId: replay.getSessionId() };
3333
}
3434

3535
// Collect traceIds in _context regardless of `recordingMode` - if it's true,
@@ -44,12 +44,10 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even
4444
replay.getContext().errorIds.add(event.event_id as string);
4545
}
4646

47-
const exc = event.exception?.values?.[0];
48-
if (__DEBUG_BUILD__ && replay.getOptions()._experiments?.traceInternals) {
47+
if (__DEBUG_BUILD__ && replay.getOptions()._experiments.traceInternals) {
48+
const exc = getEventExceptionValues(event);
4949
addInternalBreadcrumb({
50-
message: `Tagging event (${event.event_id}) - ${event.message} - ${exc?.type || 'Unknown'}: ${
51-
exc?.value || 'n/a'
52-
}`,
50+
message: `Tagging event (${event.event_id}) - ${event.message} - ${exc.type}: ${exc.value}`,
5351
});
5452
}
5553

@@ -89,3 +87,11 @@ function addInternalBreadcrumb(arg: Parameters<typeof addBreadcrumb>[0]): void {
8987
...rest,
9088
});
9189
}
90+
91+
function getEventExceptionValues(event: Event): { type: string; value: string } {
92+
return {
93+
type: 'Unknown',
94+
value: 'n/a',
95+
...(event.exception && event.exception.values && event.exception.values[0]),
96+
};
97+
}

packages/replay/src/coreHandlers/handleXhr.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null {
5050
return null;
5151
}
5252

53+
const timestamp = handlerData.xhr.__sentry_xhr__
54+
? handlerData.xhr.__sentry_xhr__.startTimestamp || 0
55+
: handlerData.endTimestamp;
56+
5357
return {
5458
type: 'resource.xhr',
5559
name: url,
56-
start: (handlerData.xhr.__sentry_xhr__?.startTimestamp || 0) / 1000 || handlerData.endTimestamp / 1000.0,
57-
end: handlerData.endTimestamp / 1000.0,
60+
start: timestamp / 1000,
61+
end: handlerData.endTimestamp / 1000,
5862
data: {
5963
method,
6064
statusCode,

packages/replay/src/eventBuffer.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
9595
*/
9696
public _pendingEvents: RecordingEvent[] = [];
9797

98-
private _worker: null | Worker;
98+
private _worker: Worker;
9999
private _eventBufferItemLength: number = 0;
100100
private _id: number = 0;
101101

@@ -124,8 +124,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
124124
*/
125125
public destroy(): void {
126126
__DEBUG_BUILD__ && logger.log('[Replay] Destroying compression worker');
127-
this._worker?.terminate();
128-
this._worker = null;
127+
this._worker.terminate();
129128
}
130129

131130
/**
@@ -177,7 +176,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
177176
}
178177

179178
// At this point, we'll always want to remove listener regardless of result status
180-
this._worker?.removeEventListener('message', listener);
179+
this._worker.removeEventListener('message', listener);
181180

182181
if (!data.success) {
183182
// TODO: Do some error handling, not sure what
@@ -200,8 +199,8 @@ export class EventBufferCompressionWorker implements EventBuffer {
200199

201200
// Note: we can't use `once` option because it's possible it needs to
202201
// listen to multiple messages
203-
this._worker?.addEventListener('message', listener);
204-
this._worker?.postMessage({ id, method, args: stringifiedArgs });
202+
this._worker.addEventListener('message', listener);
203+
this._worker.postMessage({ id, method, args: stringifiedArgs });
205204
});
206205
}
207206

packages/replay/src/replay.ts

+28-14
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ export class ReplayContainer implements ReplayContainerInterface {
231231
__DEBUG_BUILD__ && logger.log('[Replay] Stopping Replays');
232232
this._isEnabled = false;
233233
this._removeListeners();
234-
this._stopRecording?.();
235-
this.eventBuffer?.destroy();
234+
this._stopRecording && this._stopRecording();
235+
this.eventBuffer && this.eventBuffer.destroy();
236236
this.eventBuffer = null;
237237
this._debouncedFlush.cancel();
238238
} catch (err) {
@@ -278,7 +278,7 @@ export class ReplayContainer implements ReplayContainerInterface {
278278
*/
279279
public addUpdate(cb: AddUpdateCallback): void {
280280
// We need to always run `cb` (e.g. in the case of `this.recordingMode == 'error'`)
281-
const cbResult = cb?.();
281+
const cbResult = cb();
282282

283283
// If this option is turned on then we will only want to call `flush`
284284
// explicitly
@@ -335,6 +335,11 @@ export class ReplayContainer implements ReplayContainerInterface {
335335
return this._debouncedFlush.flush() as Promise<void>;
336336
}
337337

338+
/** Get the current sesion (=replay) ID */
339+
public getSessionId(): string | undefined {
340+
return this.session && this.session.id;
341+
}
342+
338343
/** A wrapper to conditionally capture exceptions. */
339344
private _handleException(error: unknown): void {
340345
__DEBUG_BUILD__ && logger.error('[Replay]', error);
@@ -363,8 +368,9 @@ export class ReplayContainer implements ReplayContainerInterface {
363368
this._setInitialState();
364369
}
365370

366-
if (session.id !== this.session?.id) {
367-
session.previousSessionId = this.session?.id;
371+
const currentSessionId = this.getSessionId();
372+
if (session.id !== currentSessionId) {
373+
session.previousSessionId = currentSessionId;
368374
}
369375

370376
this.session = session;
@@ -405,7 +411,9 @@ export class ReplayContainer implements ReplayContainerInterface {
405411
if (!this._hasInitializedCoreListeners) {
406412
// Listeners from core SDK //
407413
const scope = getCurrentHub().getScope();
408-
scope?.addScopeListener(this._handleCoreBreadcrumbListener('scope'));
414+
if (scope) {
415+
scope.addScopeListener(this._handleCoreBreadcrumbListener('scope'));
416+
}
409417
addInstrumentationHandler('dom', this._handleCoreBreadcrumbListener('dom'));
410418
addInstrumentationHandler('fetch', handleFetchSpanListener(this));
411419
addInstrumentationHandler('xhr', handleXhrSpanListener(this));
@@ -492,7 +500,7 @@ export class ReplayContainer implements ReplayContainerInterface {
492500
// of the previous session. Do not immediately flush in this case
493501
// to avoid capturing only the checkout and instead the replay will
494502
// be captured if they perform any follow-up actions.
495-
if (this.session?.previousSessionId) {
503+
if (this.session && this.session.previousSessionId) {
496504
return true;
497505
}
498506

@@ -707,7 +715,7 @@ export class ReplayContainer implements ReplayContainerInterface {
707715
* Returns true if session is not expired, false otherwise.
708716
*/
709717
private _checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void {
710-
const oldSessionId = this.session?.id;
718+
const oldSessionId = this.getSessionId();
711719

712720
// Prevent starting a new session if the last user activity is older than
713721
// MAX_SESSION_LIFE. Otherwise non-user activity can trigger a new
@@ -724,7 +732,7 @@ export class ReplayContainer implements ReplayContainerInterface {
724732
this._loadSession({ expiry });
725733

726734
// Session was expired if session ids do not match
727-
const expired = oldSessionId !== this.session?.id;
735+
const expired = oldSessionId !== this.getSessionId();
728736

729737
if (!expired) {
730738
return true;
@@ -788,20 +796,26 @@ export class ReplayContainer implements ReplayContainerInterface {
788796
* Should never be called directly, only by `flush`
789797
*/
790798
private async _runFlush(): Promise<void> {
791-
if (!this.session) {
792-
__DEBUG_BUILD__ && logger.error('[Replay] No session found to flush.');
799+
if (!this.session || !this.eventBuffer) {
800+
__DEBUG_BUILD__ && logger.error('[Replay] No session or eventBuffer found to flush.');
793801
return;
794802
}
795803

796804
await this._addPerformanceEntries();
797805

798-
if (!this.eventBuffer?.pendingLength) {
806+
// Check eventBuffer again, as it could have been stopped in the meanwhile
807+
if (!this.eventBuffer || !this.eventBuffer.pendingLength) {
799808
return;
800809
}
801810

802811
// Only attach memory event if eventBuffer is not empty
803812
await addMemoryEntry(this);
804813

814+
// Check eventBuffer again, as it could have been stopped in the meanwhile
815+
if (!this.eventBuffer) {
816+
return;
817+
}
818+
805819
try {
806820
// Note this empties the event buffer regardless of outcome of sending replay
807821
const recordingData = await this.eventBuffer.finish();
@@ -853,13 +867,13 @@ export class ReplayContainer implements ReplayContainerInterface {
853867
return;
854868
}
855869

856-
if (!this.session?.id) {
870+
if (!this.session) {
857871
__DEBUG_BUILD__ && logger.error('[Replay] No session found to flush.');
858872
return;
859873
}
860874

861875
// A flush is about to happen, cancel any queued flushes
862-
this._debouncedFlush?.cancel();
876+
this._debouncedFlush.cancel();
863877

864878
// this._flushLock acts as a lock so that future calls to `_flush()`
865879
// will be blocked until this promise resolves

packages/replay/src/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface ReplayPluginOptions extends SessionOptions {
118118
*
119119
* Default: undefined
120120
*/
121-
_experiments?: Partial<{
121+
_experiments: Partial<{
122122
captureExceptions: boolean;
123123
traceInternals: boolean;
124124
}>;
@@ -259,6 +259,7 @@ export interface ReplayContainer {
259259
triggerUserActivity(): void;
260260
addUpdate(cb: AddUpdateCallback): void;
261261
getOptions(): ReplayPluginOptions;
262+
getSessionId(): string | undefined;
262263
}
263264

264265
export interface ReplayPerformanceEntry {

packages/replay/src/util/addEvent.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function addEvent(
3535
// Only record earliest event if a new session was created, otherwise it
3636
// shouldn't be relevant
3737
const earliestEvent = replay.getContext().earliestEvent;
38-
if (replay.session?.segmentId === 0 && (!earliestEvent || timestampInMs < earliestEvent)) {
38+
if (replay.session && replay.session.segmentId === 0 && (!earliestEvent || timestampInMs < earliestEvent)) {
3939
replay.getContext().earliestEvent = timestampInMs;
4040
}
4141

0 commit comments

Comments
 (0)