Skip to content

Commit 9d6534f

Browse files
committed
ref: make type exlusive
1 parent f892179 commit 9d6534f

File tree

3 files changed

+43
-49
lines changed

3 files changed

+43
-49
lines changed

packages/core/src/exports.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
2020
import type { Hub } from './hub';
2121
import { getCurrentHub } from './hub';
2222
import type { Scope } from './scope';
23+
import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
2324
import { parseEventHintOrCaptureContext } from './utils/prepareEvent';
2425

2526
// Note: All functions in this file are typed with a return value of `ReturnType<Hub[HUB_FUNCTION]>`,
@@ -37,7 +38,7 @@ import { parseEventHintOrCaptureContext } from './utils/prepareEvent';
3738
export function captureException(
3839
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3940
exception: any,
40-
hint?: CaptureContext | EventHint,
41+
hint?: ExclusiveEventHintOrCaptureContext,
4142
): ReturnType<Hub['captureException']> {
4243
return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint));
4344
}

packages/core/src/utils/prepareEvent.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import type {
44
ClientOptions,
55
Event,
66
EventHint,
7+
Scope as ScopeInterface,
78
ScopeContext,
89
StackFrame,
910
StackParser,
1011
} from '@sentry/types';
1112
import {
1213
addExceptionMechanism,
1314
dateTimestampInSeconds,
14-
dropUndefinedKeys,
1515
GLOBAL_OBJ,
1616
normalize,
1717
resolvedSyncPromise,
@@ -23,6 +23,15 @@ import { DEFAULT_ENVIRONMENT } from '../constants';
2323
import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors';
2424
import { Scope } from '../scope';
2525

26+
/**
27+
* This type makes sure that we get either a CaptureContext, OR an EventHint.
28+
* It does not allow mixing them, which could lead to unexpected outcomes, e.g. this is disallowed:
29+
* { user: { id: '123' }, mechanism: { handled: false } }
30+
*/
31+
export type ExclusiveEventHintOrCaptureContext =
32+
| (CaptureContext & Partial<{ [key in keyof EventHint]: never }>)
33+
| (EventHint & Partial<{ [key in keyof ScopeContext]: never }>);
34+
2635
/**
2736
* Adds common information to events.
2837
*
@@ -336,7 +345,9 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number):
336345
* Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`.
337346
* This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`.
338347
*/
339-
export function parseEventHintOrCaptureContext(hint: CaptureContext | EventHint | undefined): EventHint | undefined {
348+
export function parseEventHintOrCaptureContext(
349+
hint: ExclusiveEventHintOrCaptureContext | undefined,
350+
): EventHint | undefined {
340351
if (!hint) {
341352
return undefined;
342353
}
@@ -346,24 +357,33 @@ export function parseEventHintOrCaptureContext(hint: CaptureContext | EventHint
346357
return { captureContext: hint };
347358
}
348359

349-
const hintOrScopeContext = hint as Partial<ScopeContext> & Partial<EventHint>;
350-
351-
// Else, we need to make sure to pick the legacy CaptureContext fields off & merge them into the hint
352-
const { user, level, extra, contexts, tags, fingerprint, requestSession, propagationContext, ...eventHint } =
353-
hintOrScopeContext;
354-
355-
const captureContext = {
356-
...dropUndefinedKeys({ user, level, extra, contexts, tags, fingerprint, requestSession, propagationContext }),
357-
...hintOrScopeContext.captureContext,
358-
};
359-
360-
if (Object.keys(captureContext).length) {
361-
eventHint.captureContext = captureContext;
360+
if (hintIsScopeContext(hint)) {
361+
return {
362+
captureContext: hint,
363+
};
362364
}
363365

364-
return eventHint;
366+
return hint;
365367
}
366368

367-
function hintIsScopeOrFunction(hint: CaptureContext | EventHint): hint is Scope | (() => Scope) {
369+
function hintIsScopeOrFunction(
370+
hint: CaptureContext | EventHint,
371+
): hint is ScopeInterface | ((scope: ScopeInterface) => ScopeInterface) {
368372
return hint instanceof Scope || typeof hint === 'function';
369373
}
374+
375+
type ScopeContextProperty = keyof ScopeContext;
376+
const captureContextKeys: readonly ScopeContextProperty[] = [
377+
'user',
378+
'level',
379+
'extra',
380+
'contexts',
381+
'tags',
382+
'fingerprint',
383+
'requestSession',
384+
'propagationContext',
385+
] as const;
386+
387+
function hintIsScopeContext(hint: Partial<ScopeContext> | EventHint): hint is Partial<ScopeContext> {
388+
return Object.keys(hint).some(key => captureContextKeys.includes(key as ScopeContextProperty));
389+
}

packages/core/test/lib/prepareEvent.test.ts

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Event, ScopeContext } from '@sentry/types';
1+
import type { Event, EventHint, ScopeContext } from '@sentry/types';
22
import { createStackParser, GLOBAL_OBJ } from '@sentry/utils';
33

44
import { Scope } from '../../src/scope';
@@ -130,15 +130,15 @@ describe('parseEventHintOrCaptureContext', () => {
130130
expect(actual).toEqual({ captureContext: scope });
131131
});
132132

133-
it('works with a plain EventHint', () => {
134-
const hint = {
133+
it('works with an EventHint', () => {
134+
const hint: EventHint = {
135135
mechanism: { handled: false },
136136
};
137137
const actual = parseEventHintOrCaptureContext(hint);
138138
expect(actual).toEqual(hint);
139139
});
140140

141-
it('works with a plain ScopeContext', () => {
141+
it('works with a ScopeContext', () => {
142142
const scopeContext: ScopeContext = {
143143
user: { id: 'xxx' },
144144
level: 'debug',
@@ -156,31 +156,4 @@ describe('parseEventHintOrCaptureContext', () => {
156156
const actual = parseEventHintOrCaptureContext(scopeContext);
157157
expect(actual).toEqual({ captureContext: scopeContext });
158158
});
159-
160-
it('works with a ScopeContext & event hint mixed', () => {
161-
const scopeContext: ScopeContext = {
162-
user: { id: 'xxx' },
163-
level: 'debug',
164-
extra: { foo: 'bar' },
165-
contexts: { os: { name: 'linux' } },
166-
tags: { foo: 'bar' },
167-
fingerprint: ['xx', 'yy'],
168-
requestSession: { status: 'ok' },
169-
propagationContext: {
170-
traceId: 'xxx',
171-
spanId: 'yyy',
172-
},
173-
};
174-
175-
const actual = parseEventHintOrCaptureContext({
176-
...scopeContext,
177-
mechanism: { handled: false },
178-
captureContext: { level: 'error' },
179-
});
180-
expect(actual).toEqual({
181-
// captureContext is merged, where the eventHint takes prededence
182-
captureContext: { ...scopeContext, level: 'error' },
183-
mechanism: { handled: false },
184-
});
185-
});
186159
});

0 commit comments

Comments
 (0)