sentry-vir 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,12 @@ export type EventContextAndTags = PartialWithUndefined<{
17
17
  context: EventExtraContext;
18
18
  tags: EventTags;
19
19
  attachments: ReadonlyArray<Attachment>;
20
+ /**
21
+ * Per-event throttle threshold override. When set, throttling for this event uses the minimum
22
+ * of this value and the globally-configured `throttleThreshold`, allowing individual log calls
23
+ * to be throttled more aggressively than the global default.
24
+ */
25
+ throttleThreshold: number;
20
26
  }>;
21
27
  /** Function that generates extra event context. */
22
28
  export type EventExtraContextCreator = () => EventExtraContext;
@@ -1,6 +1,6 @@
1
1
  import { type Attachment } from '@sentry/core';
2
2
  import { type EventContextAndTags, type EventExtraContext, type EventTags } from './event-context.js';
3
- import { extraEventAttachmentsSymbol, extraEventContextSymbol, extraEventTagsSymbol } from './extra-event-context.js';
3
+ import { extraEventAttachmentsSymbol, extraEventContextSymbol, extraEventTagsSymbol, extraEventThrottleThresholdSymbol } from './extra-event-context.js';
4
4
  /**
5
5
  * Constructs an error with extra event context attached to it in the same way that
6
6
  * throwWithExtraContext attaches data.
@@ -16,6 +16,7 @@ export declare class ExtraContextError extends Error {
16
16
  readonly [extraEventContextSymbol]: EventExtraContext | undefined;
17
17
  readonly [extraEventTagsSymbol]: EventTags | undefined;
18
18
  readonly [extraEventAttachmentsSymbol]: ReadonlyArray<Attachment> | undefined;
19
+ readonly [extraEventThrottleThresholdSymbol]: number | undefined;
19
20
  constructor(message: string, extraData: EventContextAndTags);
20
21
  }
21
22
  /**
@@ -1,5 +1,5 @@
1
1
  import { ensureError } from '@augment-vir/common';
2
- import { extraEventAttachmentsSymbol, extraEventContextSymbol, extraEventTagsSymbol, } from './extra-event-context.js';
2
+ import { extraEventAttachmentsSymbol, extraEventContextSymbol, extraEventTagsSymbol, extraEventThrottleThresholdSymbol, } from './extra-event-context.js';
3
3
  /**
4
4
  * Constructs an error with extra event context attached to it in the same way that
5
5
  * throwWithExtraContext attaches data.
@@ -15,6 +15,7 @@ export class ExtraContextError extends Error {
15
15
  [extraEventContextSymbol];
16
16
  [extraEventTagsSymbol];
17
17
  [extraEventAttachmentsSymbol];
18
+ [extraEventThrottleThresholdSymbol];
18
19
  constructor(message, extraData) {
19
20
  super(message);
20
21
  if (extraData.context) {
@@ -26,6 +27,9 @@ export class ExtraContextError extends Error {
26
27
  if (extraData.attachments) {
27
28
  this[extraEventAttachmentsSymbol] = extraData.attachments;
28
29
  }
30
+ if (extraData.throttleThreshold != undefined) {
31
+ this[extraEventThrottleThresholdSymbol] = extraData.throttleThreshold;
32
+ }
29
33
  }
30
34
  }
31
35
  /**
@@ -43,5 +47,8 @@ export function throwWithExtraContext(originalError, extraData) {
43
47
  if (extraData.attachments) {
44
48
  error[extraEventAttachmentsSymbol] = extraData.attachments;
45
49
  }
50
+ if (extraData.throttleThreshold != undefined) {
51
+ error[extraEventThrottleThresholdSymbol] = extraData.throttleThreshold;
52
+ }
46
53
  throw error;
47
54
  }
@@ -15,6 +15,11 @@ export declare const extraEventTagsSymbol: unique symbol;
15
15
  * for attaching file data to thrown errors.
16
16
  */
17
17
  export declare const extraEventAttachmentsSymbol: unique symbol;
18
+ /**
19
+ * Symbol used to attach a per-event throttle threshold override. When present, throttling uses the
20
+ * minimum of this value and the globally-configured `throttleThreshold`.
21
+ */
22
+ export declare const extraEventThrottleThresholdSymbol: unique symbol;
18
23
  /** Simply describes an object that has extra event context. */
19
24
  export type HasExtraContext = {
20
25
  [extraEventContextSymbol]: EventExtraContext;
@@ -27,12 +32,18 @@ export type HasExtraTags = {
27
32
  export type HasExtraAttachments = {
28
33
  [extraEventAttachmentsSymbol]: ReadonlyArray<Attachment>;
29
34
  };
35
+ /** Simply describes an object that has a per-event throttle threshold override. */
36
+ export type HasExtraThrottleThreshold = {
37
+ [extraEventThrottleThresholdSymbol]: number;
38
+ };
30
39
  /** Type guard for whether any given input has extra event context. */
31
40
  export declare function hasExtraEventContext(input: unknown): input is HasExtraContext;
32
41
  /** Type guard for whether any given input has extra event tags. */
33
42
  export declare function hasExtraEventTags(input: unknown): input is HasExtraTags;
34
43
  /** Type guard for whether any given input has extra event attachments. */
35
44
  export declare function hasExtraEventAttachments(input: unknown): input is HasExtraAttachments;
45
+ /** Type guard for whether any given input carries a per-event throttle threshold override. */
46
+ export declare function hasExtraEventThrottleThreshold(input: unknown): input is HasExtraThrottleThreshold;
36
47
  /**
37
48
  * Checks if extra event context has been injected into the input via extraEventContextSymbol and,
38
49
  * if so, extracts it.
@@ -63,3 +74,8 @@ export declare function extractExtraEventTags(event: EventHint | Event): EventTa
63
74
  * there are no extra event attachments.
64
75
  */
65
76
  export declare function extractExtraEventAttachments(event: EventHint | Event): ReadonlyArray<Attachment> | undefined;
77
+ /**
78
+ * Tries to extract a per-event throttle threshold via extraEventThrottleThresholdSymbol from the
79
+ * input itself or its originalException. Returns undefined if none is set.
80
+ */
81
+ export declare function extractExtraEventThrottleThreshold(event: EventHint | Event): number | undefined;
@@ -14,6 +14,11 @@ export const extraEventTagsSymbol = Symbol('extra-event-tags');
14
14
  * for attaching file data to thrown errors.
15
15
  */
16
16
  export const extraEventAttachmentsSymbol = Symbol('extra-event-attachments');
17
+ /**
18
+ * Symbol used to attach a per-event throttle threshold override. When present, throttling uses the
19
+ * minimum of this value and the globally-configured `throttleThreshold`.
20
+ */
21
+ export const extraEventThrottleThresholdSymbol = Symbol('extra-event-throttle-threshold');
17
22
  /** Type guard for whether any given input has extra event context. */
18
23
  export function hasExtraEventContext(input) {
19
24
  return check.hasKey(input, extraEventContextSymbol) && !!input[extraEventContextSymbol];
@@ -26,6 +31,11 @@ export function hasExtraEventTags(input) {
26
31
  export function hasExtraEventAttachments(input) {
27
32
  return check.hasKey(input, extraEventAttachmentsSymbol) && !!input[extraEventAttachmentsSymbol];
28
33
  }
34
+ /** Type guard for whether any given input carries a per-event throttle threshold override. */
35
+ export function hasExtraEventThrottleThreshold(input) {
36
+ return (check.hasKey(input, extraEventThrottleThresholdSymbol) &&
37
+ check.isNumber(input[extraEventThrottleThresholdSymbol]));
38
+ }
29
39
  /**
30
40
  * Checks if extra event context has been injected into the input via extraEventContextSymbol and,
31
41
  * if so, extracts it.
@@ -122,3 +132,16 @@ export function extractExtraEventAttachments(event) {
122
132
  return undefined;
123
133
  }
124
134
  }
135
+ /**
136
+ * Tries to extract a per-event throttle threshold via extraEventThrottleThresholdSymbol from the
137
+ * input itself or its originalException. Returns undefined if none is set.
138
+ */
139
+ export function extractExtraEventThrottleThreshold(event) {
140
+ const fromRoot = hasExtraEventThrottleThreshold(event)
141
+ ? event[extraEventThrottleThresholdSymbol]
142
+ : undefined;
143
+ const fromException = 'originalException' in event && hasExtraEventThrottleThreshold(event.originalException)
144
+ ? event.originalException[extraEventThrottleThresholdSymbol]
145
+ : undefined;
146
+ return fromRoot ?? fromException;
147
+ }
@@ -1,5 +1,6 @@
1
1
  import { setSentryClientForLogging } from '../logging/sentry-client-for-logging.js';
2
2
  import { processSentryEvent } from '../processing/event-processor.js';
3
+ import { setActiveThrottleOptions } from '../processing/throttling.js';
3
4
  import { createSentryConfig } from './sentry-config.js';
4
5
  /**
5
6
  * Base Sentry init. Requires the Sentry module to already have been imported. Setup a sentry client
@@ -25,6 +26,7 @@ export async function baseInitSentry({ dsn, releaseEnv, releaseName, sentryConfi
25
26
  });
26
27
  sentryDep.init(finalSentryConfig);
27
28
  sentryDep.addEventProcessor((event, hint) => processSentryEvent(event, hint, createUniversalContext));
29
+ setActiveThrottleOptions(throttleOptions);
28
30
  void setSentryClientForLogging(sentryDep);
29
31
  return sentryDep;
30
32
  }
@@ -1,9 +1,11 @@
1
1
  import { extractErrorMessage } from '@augment-vir/common';
2
2
  import { convertEventDetailsToSentryContext, } from '../event-context/event-context.js';
3
3
  import { EventSeverityEnum } from '../event-context/event-severity.js';
4
- import { extractExtraAttachmentsFromSymbol } from '../event-context/extra-event-context.js';
4
+ import { extractExtraAttachmentsFromSymbol, extractExtraEventThrottleThreshold, } from '../event-context/extra-event-context.js';
5
5
  import { LoggingState, logToConsoleWithoutSentry } from '../processing/log-to-console.js';
6
+ import { skipBeforeSendThrottleContextKey } from '../processing/throttling.js';
6
7
  import { addPrematureEvent } from './premature-events.js';
8
+ import { checkActiveThrottle } from './send-log.js';
7
9
  import { sentryClientForLogging } from './sentry-client-for-logging.js';
8
10
  /** Record an error to Sentry without throwing it. */
9
11
  export function handleError(error, eventOptions) {
@@ -30,6 +32,22 @@ function internalHandleError(error, eventOptions, options) {
30
32
  ]);
31
33
  return undefined;
32
34
  }
35
+ const perCallThresholdCandidates = [
36
+ extractExtraEventThrottleThreshold({
37
+ originalException: error,
38
+ }),
39
+ eventOptions?.throttleThreshold,
40
+ ].filter((value) => value != undefined);
41
+ const perCallThrottleThreshold = perCallThresholdCandidates.length
42
+ ? Math.min(...perCallThresholdCandidates)
43
+ : undefined;
44
+ if (checkActiveThrottle({
45
+ message: extractErrorMessage(error),
46
+ }, {
47
+ originalException: error,
48
+ }, perCallThrottleThreshold)) {
49
+ return undefined;
50
+ }
33
51
  const scopeContext = convertEventDetailsToSentryContext({
34
52
  extraContext: eventOptions?.context,
35
53
  tags: eventOptions?.tags,
@@ -40,14 +58,15 @@ function internalHandleError(error, eventOptions, options) {
40
58
  ...(eventOptions?.attachments || []),
41
59
  ...(extractExtraAttachmentsFromSymbol(error) || []),
42
60
  ];
43
- const eventId = allAttachments.length
44
- ? client.withScope((scope) => {
45
- allAttachments.forEach((attachment) => {
46
- scope.addAttachment(attachment);
47
- });
48
- return client.captureException(error, scopeContext);
49
- })
50
- : client.captureException(error, scopeContext);
61
+ const eventId = client.withScope((scope) => {
62
+ scope.setContext(skipBeforeSendThrottleContextKey, {
63
+ skipThrottle: true,
64
+ });
65
+ allAttachments.forEach((attachment) => {
66
+ scope.addAttachment(attachment);
67
+ });
68
+ return client.captureException(error, scopeContext);
69
+ });
51
70
  return eventId;
52
71
  }
53
72
  catch (caught) {
@@ -1,5 +1,7 @@
1
- import { type Event as SentryEvent } from '@sentry/core';
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type ErrorEvent, type EventHint, type Event as SentryEvent, type TransactionEvent } from '@sentry/core';
2
3
  import { type ContextOptions, type EventContextAndTags, type EventDetails } from '../event-context/event-context.js';
4
+ import { type ThrottleOptions } from '../processing/throttling.js';
3
5
  /** A Sentry event object without the fields that are set by the logging context. */
4
6
  export type SendLogEvent = Omit<SentryEvent, 'extra' | 'level'>;
5
7
  /** All accepted input types for `sendLog`. Strings, Error objects, and raw Sentry events. */
@@ -13,5 +15,22 @@ export declare const sendLog: {
13
15
  /** Sends an even to Sentry with warning severity. */
14
16
  readonly warning: (info: Parameters<typeof sendLogToSentry>[0], eventOptions?: EventContextAndTags) => string | undefined;
15
17
  };
16
- declare function sendLogToSentry(logInfo: SendLogInfo, eventDetails: EventDetails, options: ContextOptions): string | undefined;
18
+ /**
19
+ * Runs the throttle decision and, if a state transition just occurred (`'started'` or `'ended'`)
20
+ * and `disableThrottleLog` is not set, emits the corresponding `Throttling started: ...` /
21
+ * `Throttling ended after suppressing N events: ...` warning via `sendLog.warning`. Returns whether
22
+ * the event should be throttled (i.e. dropped).
23
+ *
24
+ * @category Internal
25
+ */
26
+ export declare function throttleEventWithLogging(event: Pick<TransactionEvent | ErrorEvent, 'message'>, hint: Readonly<Pick<EventHint, 'originalException'>> | undefined, options: Readonly<PartialWithUndefined<ThrottleOptions>>): boolean;
27
+ /**
28
+ * Synchronous pre-capture throttle check used by `sendLog` and `handleError`. Returns `true` when
29
+ * the event should be dropped. Returns `false` (i.e. "send it") when no active throttle options
30
+ * have been registered yet, so events sent before Sentry init aren't accidentally throttled.
31
+ *
32
+ * @category Internal
33
+ */
34
+ export declare function checkActiveThrottle(event: Pick<TransactionEvent | ErrorEvent, 'message'>, hint: Readonly<Pick<EventHint, 'originalException'>> | undefined, perCallThreshold: number | undefined): boolean;
35
+ declare function sendLogToSentry(logInfo: SendLogInfo, eventDetails: EventDetails, options: ContextOptions, perCallThrottleThreshold: number | undefined): string | undefined;
17
36
  export {};
@@ -4,6 +4,7 @@ import { convertEventDetailsToSentryContext, } from '../event-context/event-cont
4
4
  import { EventSeverityEnum } from '../event-context/event-severity.js';
5
5
  import { extractOriginalMessage } from '../processing/event-processor.js';
6
6
  import { LoggingState, logToConsoleWithoutSentry } from '../processing/log-to-console.js';
7
+ import { combineThrottleThreshold, defaultThrottleOptions, getActiveThrottleOptions, shouldThrottleEvent, skipBeforeSendThrottleContextKey, } from '../processing/throttling.js';
7
8
  import { addPrematureEvent } from './premature-events.js';
8
9
  import { sentryClientForLogging } from './sentry-client-for-logging.js';
9
10
  /** Send non-error events to Sentry. */
@@ -15,6 +16,56 @@ export const sendLog = {
15
16
  /** Sends an even to Sentry with warning severity. */
16
17
  [EventSeverityEnum.Warning]: wrapLogWithSeverity(EventSeverityEnum.Warning),
17
18
  };
19
+ /**
20
+ * Runs the throttle decision and, if a state transition just occurred (`'started'` or `'ended'`)
21
+ * and `disableThrottleLog` is not set, emits the corresponding `Throttling started: ...` /
22
+ * `Throttling ended after suppressing N events: ...` warning via `sendLog.warning`. Returns whether
23
+ * the event should be throttled (i.e. dropped).
24
+ *
25
+ * @category Internal
26
+ */
27
+ export function throttleEventWithLogging(event, hint, options) {
28
+ const result = shouldThrottleEvent(event, hint, options);
29
+ const disableLog = options.disableThrottleLog ?? defaultThrottleOptions.disableThrottleLog;
30
+ if (!disableLog && result.errorKey != undefined) {
31
+ if (result.transition.kind === 'started') {
32
+ sendLog.warning(`Throttling started: ${result.errorKey}`, {
33
+ context: {
34
+ suppressedErrorKey: result.errorKey,
35
+ },
36
+ tags: {
37
+ suppressedErrorKey: result.errorKey,
38
+ },
39
+ });
40
+ }
41
+ else if (result.transition.kind === 'ended') {
42
+ sendLog.warning(`Throttling ended after suppressing ${result.transition.suppressedCount} events: ${result.errorKey}`, {
43
+ context: {
44
+ suppressedErrorKey: result.errorKey,
45
+ suppressedCount: result.transition.suppressedCount,
46
+ },
47
+ tags: {
48
+ suppressedErrorKey: result.errorKey,
49
+ },
50
+ });
51
+ }
52
+ }
53
+ return result.shouldThrottle;
54
+ }
55
+ /**
56
+ * Synchronous pre-capture throttle check used by `sendLog` and `handleError`. Returns `true` when
57
+ * the event should be dropped. Returns `false` (i.e. "send it") when no active throttle options
58
+ * have been registered yet, so events sent before Sentry init aren't accidentally throttled.
59
+ *
60
+ * @category Internal
61
+ */
62
+ export function checkActiveThrottle(event, hint, perCallThreshold) {
63
+ const active = getActiveThrottleOptions();
64
+ if (!active) {
65
+ return false;
66
+ }
67
+ return throttleEventWithLogging(event, hint, combineThrottleThreshold(active, perCallThreshold));
68
+ }
18
69
  function wrapLogWithSeverity(severity) {
19
70
  return (info, eventOptions) => {
20
71
  return sendLogToSentry(info, {
@@ -24,10 +75,10 @@ function wrapLogWithSeverity(severity) {
24
75
  severity,
25
76
  }, {
26
77
  wasSentPrematurely: false,
27
- });
78
+ }, eventOptions?.throttleThreshold);
28
79
  };
29
80
  }
30
- function sendLogToSentry(logInfo, eventDetails, options) {
81
+ function sendLogToSentry(logInfo, eventDetails, options, perCallThrottleThreshold) {
31
82
  try {
32
83
  /**
33
84
  * `Error.message` is not enumerable, so spreading an `Error` into `captureEvent` would lose
@@ -51,9 +102,18 @@ function sendLogToSentry(logInfo, eventDetails, options) {
51
102
  {
52
103
  wasSentPrematurely: true,
53
104
  },
105
+ perCallThrottleThreshold,
54
106
  ]);
55
107
  return undefined;
56
108
  }
109
+ const throttleMessage = check.isString(resolvedLogInfo)
110
+ ? resolvedLogInfo
111
+ : extractOriginalMessage(resolvedLogInfo, undefined);
112
+ if (checkActiveThrottle({
113
+ message: throttleMessage,
114
+ }, undefined, perCallThrottleThreshold)) {
115
+ return undefined;
116
+ }
57
117
  const scopeContext = convertEventDetailsToSentryContext(eventDetails, options);
58
118
  const client = sentryClientForLogging;
59
119
  function captureWithClient() {
@@ -64,14 +124,15 @@ function sendLogToSentry(logInfo, eventDetails, options) {
64
124
  ...scopeContext,
65
125
  });
66
126
  }
67
- const eventId = eventDetails.attachments?.length
68
- ? client.withScope((scope) => {
69
- eventDetails.attachments?.forEach((attachment) => {
70
- scope.addAttachment(attachment);
71
- });
72
- return captureWithClient();
73
- })
74
- : captureWithClient();
127
+ const eventId = client.withScope((scope) => {
128
+ scope.setContext(skipBeforeSendThrottleContextKey, {
129
+ skipThrottle: true,
130
+ });
131
+ eventDetails.attachments?.forEach((attachment) => {
132
+ scope.addAttachment(attachment);
133
+ });
134
+ return captureWithClient();
135
+ });
75
136
  return eventId;
76
137
  }
77
138
  catch (caught) {
@@ -1,5 +1,7 @@
1
+ import { extractExtraEventThrottleThreshold } from '../event-context/extra-event-context.js';
2
+ import { throttleEventWithLogging } from '../logging/send-log.js';
1
3
  import { LoggingState, logToConsoleFromSentry } from './log-to-console.js';
2
- import { shouldThrottleEvent } from './throttling.js';
4
+ import { combineThrottleThreshold, skipBeforeSendThrottleContextKey, } from './throttling.js';
3
5
  /** Creates a handler for Sentry events based on the given env. */
4
6
  export function createSentryHandler({ isDev, isSilent, throttleOptions, }) {
5
7
  /** The actual function that gets called when handling Sentry events. */
@@ -8,8 +10,15 @@ export function createSentryHandler({ isDev, isSilent, throttleOptions, }) {
8
10
  event,
9
11
  /** The EventHint generated by Sentry. */
10
12
  hint) {
11
- if (shouldThrottleEvent(event, hint, throttleOptions)) {
12
- return null;
13
+ const wasPreThrottled = event.contexts?.[skipBeforeSendThrottleContextKey]?.skipThrottle === true;
14
+ if (event.contexts && skipBeforeSendThrottleContextKey in event.contexts) {
15
+ delete event.contexts[skipBeforeSendThrottleContextKey];
16
+ }
17
+ if (!wasPreThrottled) {
18
+ const perEventThreshold = extractExtraEventThrottleThreshold(hint);
19
+ if (throttleEventWithLogging(event, hint, combineThrottleThreshold(throttleOptions, perEventThreshold))) {
20
+ return null;
21
+ }
13
22
  }
14
23
  if (!event.extra?.wasSentPrematurely) {
15
24
  logToConsoleFromSentry(event, hint, isDev ? LoggingState.Dev : LoggingState.Prod, isSilent);
@@ -49,7 +49,36 @@ export type ThrottleOptions = {
49
49
  */
50
50
  export declare const defaultThrottleOptions: ThrottleOptions;
51
51
  /**
52
- * Determines if an event should be throttled based on previous event counts.
52
+ * The state transition (if any) that just occurred for an error key. Used by callers to decide
53
+ * whether to emit a `"Throttling started"` or `"Throttling ended after suppressing N events"` log.
54
+ *
55
+ * @category Internal
56
+ */
57
+ export type ThrottleTransition = {
58
+ kind: 'none';
59
+ } | {
60
+ kind: 'started';
61
+ } | {
62
+ kind: 'ended';
63
+ suppressedCount: number;
64
+ };
65
+ /**
66
+ * Result of a single {@link shouldThrottleEvent} call: whether the event should be dropped, the
67
+ * fuzzy cluster key it was bucketed into, and the state transition (if any) that the caller may
68
+ * want to surface as a log.
69
+ *
70
+ * @category Internal
71
+ */
72
+ export type ThrottleResult = {
73
+ shouldThrottle: boolean;
74
+ /** Undefined when throttling is disabled (the event isn't bucketed). */
75
+ errorKey: FuzzyIndexKey | undefined;
76
+ transition: ThrottleTransition;
77
+ };
78
+ /**
79
+ * Determines whether an event should be throttled based on previous event counts.This does not emit
80
+ * any logs itself, the caller is expected to interpret `result.transition` and emit logs as
81
+ * appropriate.
53
82
  *
54
83
  * @category Internal
55
84
  */
@@ -57,4 +86,34 @@ export declare function shouldThrottleEvent(
57
86
  /** Event from Sentry. */
58
87
  event: Pick<TransactionEvent | ErrorEvent, 'message'>,
59
88
  /** EventHint generated by Sentry. */
60
- hint?: Readonly<Pick<EventHint, 'originalException'>> | undefined, userOptions?: Readonly<PartialWithUndefined<ThrottleOptions>>): boolean;
89
+ hint?: Readonly<Pick<EventHint, 'originalException'>> | undefined, userOptions?: Readonly<PartialWithUndefined<ThrottleOptions>>): ThrottleResult;
90
+ /**
91
+ * Reserved key under `event.contexts` that `sendLog` and `handleError` set via `withScope` to tell
92
+ * `handleSentrySend` to skip its own throttle check — they've already done the check synchronously
93
+ * and the event passing through is one they chose to forward. Stripped from the event in
94
+ * `handleSentrySend` so it never reaches Sentry.
95
+ *
96
+ * @category Internal
97
+ */
98
+ export declare const skipBeforeSendThrottleContextKey = "__sentryVirSkipThrottle";
99
+ /**
100
+ * Sets the throttle options consulted by `shouldThrottleSync` (in `send-log.ts`). Called by
101
+ * `baseInitSentry`.
102
+ *
103
+ * @category Internal
104
+ */
105
+ export declare function setActiveThrottleOptions(options: Readonly<PartialWithUndefined<ThrottleOptions>> | undefined): void;
106
+ /**
107
+ * Returns the throttle options registered by the most recent `baseInitSentry` call, or `undefined`
108
+ * if Sentry has not yet been initialized.
109
+ *
110
+ * @category Internal
111
+ */
112
+ export declare function getActiveThrottleOptions(): Readonly<PartialWithUndefined<ThrottleOptions>> | undefined;
113
+ /**
114
+ * Combines a per-call throttle threshold with the active global throttle options, taking the
115
+ * minimum of the two so the per-call threshold can only ever tighten throttling.
116
+ *
117
+ * @category Internal
118
+ */
119
+ export declare function combineThrottleThreshold(base: Readonly<PartialWithUndefined<ThrottleOptions>> | undefined, perCallThreshold: number | undefined): Readonly<PartialWithUndefined<ThrottleOptions>>;
@@ -1,7 +1,6 @@
1
1
  import { getOrSetFromMap, mergeDefinedProperties, } from '@augment-vir/common';
2
2
  import { calculateRelativeDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
3
3
  import { FuzzyIndex } from 'fuzzy-vir';
4
- import { sendLog } from '../logging/send-log.js';
5
4
  import { extractOriginalMessage } from './event-processor.js';
6
5
  /**
7
6
  * The current throttle cache, keyed by a fuzzy cluster key so near-duplicate error messages share a
@@ -35,7 +34,9 @@ export const defaultThrottleOptions = {
35
34
  throttleThreshold: 500,
36
35
  };
37
36
  /**
38
- * Determines if an event should be throttled based on previous event counts.
37
+ * Determines whether an event should be throttled based on previous event counts.This does not emit
38
+ * any logs itself, the caller is expected to interpret `result.transition` and emit logs as
39
+ * appropriate.
39
40
  *
40
41
  * @category Internal
41
42
  */
@@ -46,7 +47,13 @@ event,
46
47
  hint, userOptions = defaultThrottleOptions) {
47
48
  const options = mergeDefinedProperties(defaultThrottleOptions, userOptions);
48
49
  if (options.disableThrottling) {
49
- return false;
50
+ return {
51
+ shouldThrottle: false,
52
+ errorKey: undefined,
53
+ transition: {
54
+ kind: 'none',
55
+ },
56
+ };
50
57
  }
51
58
  const errorKey = fuzzyErrorIndex.insert(extractOriginalMessage(event, hint));
52
59
  const now = getNowInUtcTimezone();
@@ -56,21 +63,17 @@ hint, userOptions = defaultThrottleOptions) {
56
63
  intervalStartAt: now,
57
64
  };
58
65
  });
66
+ const transitionCandidates = [];
59
67
  const intervalNeedsRestart = isDateAfter({
60
68
  fullDate: now,
61
69
  relativeTo: calculateRelativeDate(errorThrottleData.intervalStartAt, options.thresholdInterval),
62
70
  });
63
71
  if (intervalNeedsRestart) {
64
72
  const suppressedCount = errorThrottleData.intervalCount - options.throttleThreshold;
65
- if (suppressedCount > 0 && !options.disableThrottleLog) {
66
- sendLog.warning(`Throttling ended after suppressing ${suppressedCount} events: ${errorKey}`, {
67
- context: {
68
- suppressedErrorKey: errorKey,
69
- suppressedCount,
70
- },
71
- tags: {
72
- suppressedErrorKey: errorKey,
73
- },
73
+ if (suppressedCount > 0) {
74
+ transitionCandidates.push({
75
+ kind: 'ended',
76
+ suppressedCount,
74
77
  });
75
78
  }
76
79
  errorThrottleData.intervalStartAt = now;
@@ -78,17 +81,67 @@ hint, userOptions = defaultThrottleOptions) {
78
81
  }
79
82
  errorThrottleData.intervalCount++;
80
83
  const shouldThrottle = errorThrottleData.intervalCount > options.throttleThreshold;
81
- if (shouldThrottle &&
82
- errorThrottleData.intervalCount === options.throttleThreshold + 1 &&
83
- !options.disableThrottleLog) {
84
- sendLog.warning(`Throttling started: ${errorKey}`, {
85
- context: {
86
- suppressedErrorKey: errorKey,
87
- },
88
- tags: {
89
- suppressedErrorKey: errorKey,
90
- },
84
+ if (shouldThrottle && errorThrottleData.intervalCount === options.throttleThreshold + 1) {
85
+ transitionCandidates.push({
86
+ kind: 'started',
91
87
  });
92
88
  }
93
- return shouldThrottle;
89
+ return {
90
+ shouldThrottle,
91
+ errorKey,
92
+ transition: transitionCandidates[0] ?? {
93
+ kind: 'none',
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * Reserved key under `event.contexts` that `sendLog` and `handleError` set via `withScope` to tell
99
+ * `handleSentrySend` to skip its own throttle check — they've already done the check synchronously
100
+ * and the event passing through is one they chose to forward. Stripped from the event in
101
+ * `handleSentrySend` so it never reaches Sentry.
102
+ *
103
+ * @category Internal
104
+ */
105
+ export const skipBeforeSendThrottleContextKey = '__sentryVirSkipThrottle';
106
+ /**
107
+ * The throttle options currently in effect, set by {@link setActiveThrottleOptions} during Sentry
108
+ * initialization. Used by `sendLog` and `handleError` for their synchronous pre-capture throttle
109
+ * checks. `undefined` means Sentry has not yet been initialized.
110
+ *
111
+ * @category Internal
112
+ */
113
+ let activeThrottleOptions;
114
+ /**
115
+ * Sets the throttle options consulted by `shouldThrottleSync` (in `send-log.ts`). Called by
116
+ * `baseInitSentry`.
117
+ *
118
+ * @category Internal
119
+ */
120
+ export function setActiveThrottleOptions(options) {
121
+ activeThrottleOptions = options;
122
+ }
123
+ /**
124
+ * Returns the throttle options registered by the most recent `baseInitSentry` call, or `undefined`
125
+ * if Sentry has not yet been initialized.
126
+ *
127
+ * @category Internal
128
+ */
129
+ export function getActiveThrottleOptions() {
130
+ return activeThrottleOptions;
131
+ }
132
+ /**
133
+ * Combines a per-call throttle threshold with the active global throttle options, taking the
134
+ * minimum of the two so the per-call threshold can only ever tighten throttling.
135
+ *
136
+ * @category Internal
137
+ */
138
+ export function combineThrottleThreshold(base, perCallThreshold) {
139
+ if (perCallThreshold == undefined) {
140
+ return base ?? {};
141
+ }
142
+ const baseThreshold = base?.throttleThreshold ?? defaultThrottleOptions.throttleThreshold;
143
+ return {
144
+ ...base,
145
+ throttleThreshold: Math.min(baseThreshold, perCallThreshold),
146
+ };
94
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentry-vir",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Easily use Sentry.",
5
5
  "keywords": [
6
6
  "config",