@loglayer/mixin-wide-events 1.2.1 → 1.4.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.
package/dist/index.cjs CHANGED
@@ -94,7 +94,8 @@ function createWideEventMixin(options) {
94
94
  rate: options.sampling.rate,
95
95
  perLevel: snapshotPerLevel,
96
96
  emitLevel: options.sampling.emitLevel,
97
- shouldEmit: options.sampling.shouldEmit
97
+ shouldEmit: options.sampling.shouldEmit,
98
+ forceKeep: options.sampling.forceKeep
98
99
  } : void 0;
99
100
  function defaultErrorSerializer(err) {
100
101
  if (err instanceof Error) return {
@@ -189,23 +190,59 @@ function createWideEventMixin(options) {
189
190
  return self;
190
191
  }
191
192
  /**
193
+ * Logs a thrown sampling callback via console.error, gated on `consoleDebug`.
194
+ * Matches loglayer's emit-path error convention (see LogLayer.ts:1457).
195
+ */
196
+ function logCallbackError(callbackName, err, self) {
197
+ if (self.getConfig?.()?.consoleDebug) console.error(`[LogLayer] wide-events ${callbackName} callback threw; falling back:`, err);
198
+ }
199
+ /**
200
+ * Single sampling decision for an emit. Only called when a `forceKeep` or
201
+ * `shouldEmit` callback is configured (the no-callback case is handled by the
202
+ * pre-build fast-path in emitWideEventImpl).
203
+ *
204
+ * - `forceKeep` runs first (OR logic): true → keep; throws → fail-safe (log +
205
+ * fall through); false → fall through.
206
+ * - Standard logic: rate AND `shouldEmit` (shouldEmit throws → fail-open, log
207
+ * + keep). Without `shouldEmit`, rate alone decides.
208
+ *
209
+ * Invokes `runRateSampling` at most once.
210
+ */
211
+ function evaluateSampling(wideData, level, self) {
212
+ if (samplingConfig?.forceKeep) try {
213
+ if (samplingConfig.forceKeep({
214
+ wideData,
215
+ level
216
+ })) return true;
217
+ } catch (err) {
218
+ logCallbackError("forceKeep", err, self);
219
+ }
220
+ if (samplingConfig?.shouldEmit) try {
221
+ const callbackOk = samplingConfig.shouldEmit({
222
+ wideData,
223
+ level
224
+ });
225
+ return runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) && callbackOk;
226
+ } catch (err) {
227
+ logCallbackError("shouldEmit", err, self);
228
+ return true;
229
+ }
230
+ return runRateSampling(level, samplingConfig?.strategy, samplingConfig?.rate, samplingConfig?.perLevel);
231
+ }
232
+ /**
192
233
  * Implementation for emitWideEvent
193
234
  */
194
235
  function emitWideEventImpl(config, self) {
195
236
  const defaultLevel = samplingConfig?.emitLevel ?? "info";
196
237
  const level = config.level ?? defaultLevel;
197
- if (samplingConfig && !samplingConfig.shouldEmit && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
238
+ if (samplingConfig && !samplingConfig.shouldEmit && !samplingConfig.forceKeep && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
198
239
  const store = asyncContext.getStore();
199
240
  const wideEventData = {};
200
241
  if (includeContext && store?._llContext) Object.assign(wideEventData, store._llContext);
201
242
  if (store?._llWideEvents) Object.assign(wideEventData, store._llWideEvents);
202
- if (samplingConfig?.shouldEmit) try {
203
- const callbackOk = samplingConfig.shouldEmit({
204
- wideData: wideEventData,
205
- level
206
- });
207
- if (!runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) || !callbackOk) return;
208
- } catch {}
243
+ if (samplingConfig && (samplingConfig.forceKeep || samplingConfig.shouldEmit)) {
244
+ if (!evaluateSampling(wideEventData, level, self)) return;
245
+ }
209
246
  const metadataToEmit = wideEventField ? { [wideEventField]: wideEventData } : wideEventData;
210
247
  self.raw({
211
248
  logLevel: level,
package/dist/index.d.cts CHANGED
@@ -107,6 +107,37 @@ interface WideEventSamplingConfig {
107
107
  * ```
108
108
  */
109
109
  shouldEmit?: (params: WideEventSamplingParams) => boolean;
110
+ /**
111
+ * A keep-only override evaluated **before** the rate/`shouldEmit` checks.
112
+ * When it returns `true`, the event is emitted immediately, bypassing the
113
+ * rate check and `shouldEmit`. When it returns `false`, standard
114
+ * rate/`shouldEmit` logic applies. Receives the same params as `shouldEmit`,
115
+ * so any external signal (e.g. an HTTP header) must first be written into the
116
+ * wide event via `withWideEvents()`.
117
+ *
118
+ * `forceKeep` can only rescue events — it never drops an event that standard
119
+ * logic would keep (`error`/`fatal` stay exempt).
120
+ *
121
+ * Treat `wideData` as read-only: it is passed by reference and mutating it
122
+ * changes what gets emitted.
123
+ *
124
+ * If it throws, it fails **safe**: the throw is swallowed and evaluation
125
+ * falls through to the standard rate/`shouldEmit` logic (unlike `shouldEmit`,
126
+ * which fails open). A swallowed throw is logged via `console.error` when
127
+ * `consoleDebug` is enabled.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * {
132
+ * strategy: "per_level",
133
+ * perLevel: { info: 0.01 },
134
+ * forceKeep: ({ wideData }) =>
135
+ * wideData?.debug === true ||
136
+ * (wideData?.deps?.someService?.failures ?? 0) > 0,
137
+ * }
138
+ * ```
139
+ */
140
+ forceKeep?: (params: WideEventSamplingParams) => boolean;
110
141
  /**
111
142
  * Override the default log level when no explicit `level` is passed to
112
143
  * `emitWideEvent()`. When set, the resolved level uses this value instead
package/dist/index.d.mts CHANGED
@@ -107,6 +107,37 @@ interface WideEventSamplingConfig {
107
107
  * ```
108
108
  */
109
109
  shouldEmit?: (params: WideEventSamplingParams) => boolean;
110
+ /**
111
+ * A keep-only override evaluated **before** the rate/`shouldEmit` checks.
112
+ * When it returns `true`, the event is emitted immediately, bypassing the
113
+ * rate check and `shouldEmit`. When it returns `false`, standard
114
+ * rate/`shouldEmit` logic applies. Receives the same params as `shouldEmit`,
115
+ * so any external signal (e.g. an HTTP header) must first be written into the
116
+ * wide event via `withWideEvents()`.
117
+ *
118
+ * `forceKeep` can only rescue events — it never drops an event that standard
119
+ * logic would keep (`error`/`fatal` stay exempt).
120
+ *
121
+ * Treat `wideData` as read-only: it is passed by reference and mutating it
122
+ * changes what gets emitted.
123
+ *
124
+ * If it throws, it fails **safe**: the throw is swallowed and evaluation
125
+ * falls through to the standard rate/`shouldEmit` logic (unlike `shouldEmit`,
126
+ * which fails open). A swallowed throw is logged via `console.error` when
127
+ * `consoleDebug` is enabled.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * {
132
+ * strategy: "per_level",
133
+ * perLevel: { info: 0.01 },
134
+ * forceKeep: ({ wideData }) =>
135
+ * wideData?.debug === true ||
136
+ * (wideData?.deps?.someService?.failures ?? 0) > 0,
137
+ * }
138
+ * ```
139
+ */
140
+ forceKeep?: (params: WideEventSamplingParams) => boolean;
110
141
  /**
111
142
  * Override the default log level when no explicit `level` is passed to
112
143
  * `emitWideEvent()`. When set, the resolved level uses this value instead
package/dist/index.mjs CHANGED
@@ -93,7 +93,8 @@ function createWideEventMixin(options) {
93
93
  rate: options.sampling.rate,
94
94
  perLevel: snapshotPerLevel,
95
95
  emitLevel: options.sampling.emitLevel,
96
- shouldEmit: options.sampling.shouldEmit
96
+ shouldEmit: options.sampling.shouldEmit,
97
+ forceKeep: options.sampling.forceKeep
97
98
  } : void 0;
98
99
  function defaultErrorSerializer(err) {
99
100
  if (err instanceof Error) return {
@@ -188,23 +189,59 @@ function createWideEventMixin(options) {
188
189
  return self;
189
190
  }
190
191
  /**
192
+ * Logs a thrown sampling callback via console.error, gated on `consoleDebug`.
193
+ * Matches loglayer's emit-path error convention (see LogLayer.ts:1457).
194
+ */
195
+ function logCallbackError(callbackName, err, self) {
196
+ if (self.getConfig?.()?.consoleDebug) console.error(`[LogLayer] wide-events ${callbackName} callback threw; falling back:`, err);
197
+ }
198
+ /**
199
+ * Single sampling decision for an emit. Only called when a `forceKeep` or
200
+ * `shouldEmit` callback is configured (the no-callback case is handled by the
201
+ * pre-build fast-path in emitWideEventImpl).
202
+ *
203
+ * - `forceKeep` runs first (OR logic): true → keep; throws → fail-safe (log +
204
+ * fall through); false → fall through.
205
+ * - Standard logic: rate AND `shouldEmit` (shouldEmit throws → fail-open, log
206
+ * + keep). Without `shouldEmit`, rate alone decides.
207
+ *
208
+ * Invokes `runRateSampling` at most once.
209
+ */
210
+ function evaluateSampling(wideData, level, self) {
211
+ if (samplingConfig?.forceKeep) try {
212
+ if (samplingConfig.forceKeep({
213
+ wideData,
214
+ level
215
+ })) return true;
216
+ } catch (err) {
217
+ logCallbackError("forceKeep", err, self);
218
+ }
219
+ if (samplingConfig?.shouldEmit) try {
220
+ const callbackOk = samplingConfig.shouldEmit({
221
+ wideData,
222
+ level
223
+ });
224
+ return runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) && callbackOk;
225
+ } catch (err) {
226
+ logCallbackError("shouldEmit", err, self);
227
+ return true;
228
+ }
229
+ return runRateSampling(level, samplingConfig?.strategy, samplingConfig?.rate, samplingConfig?.perLevel);
230
+ }
231
+ /**
191
232
  * Implementation for emitWideEvent
192
233
  */
193
234
  function emitWideEventImpl(config, self) {
194
235
  const defaultLevel = samplingConfig?.emitLevel ?? "info";
195
236
  const level = config.level ?? defaultLevel;
196
- if (samplingConfig && !samplingConfig.shouldEmit && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
237
+ if (samplingConfig && !samplingConfig.shouldEmit && !samplingConfig.forceKeep && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
197
238
  const store = asyncContext.getStore();
198
239
  const wideEventData = {};
199
240
  if (includeContext && store?._llContext) Object.assign(wideEventData, store._llContext);
200
241
  if (store?._llWideEvents) Object.assign(wideEventData, store._llWideEvents);
201
- if (samplingConfig?.shouldEmit) try {
202
- const callbackOk = samplingConfig.shouldEmit({
203
- wideData: wideEventData,
204
- level
205
- });
206
- if (!runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) || !callbackOk) return;
207
- } catch {}
242
+ if (samplingConfig && (samplingConfig.forceKeep || samplingConfig.shouldEmit)) {
243
+ if (!evaluateSampling(wideEventData, level, self)) return;
244
+ }
208
245
  const metadataToEmit = wideEventField ? { [wideEventField]: wideEventData } : wideEventData;
209
246
  self.raw({
210
247
  logLevel: level,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loglayer/mixin-wide-events",
3
3
  "description": "Adds wide event logging functionality to LogLayer for comprehensive, self-contained log entries.",
4
- "version": "1.2.1",
4
+ "version": "1.4.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.mjs",
@@ -38,8 +38,8 @@
38
38
  "typescript": "6.0.3",
39
39
  "vitest": "4.1.7",
40
40
  "@internal/tsconfig": "2.1.0",
41
- "@loglayer/shared": "4.3.0",
42
- "loglayer": "9.3.0"
41
+ "@loglayer/shared": "4.4.0",
42
+ "loglayer": "9.4.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "loglayer": ">=9.0.0"