@loglayer/mixin-wide-events 1.0.0 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Theo Gravity / [Disaresta](https://disaresta.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -1,5 +1,4 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
2
  //#region ../../core/plugin/dist/index.js
4
3
  /**
5
4
  * List of plugin callbacks that can be called by the plugin manager.
@@ -20,17 +19,7 @@ let PluginCallbackType = /* @__PURE__ */ function(PluginCallbackType) {
20
19
  PluginCallbackType["onContextCalled"] = "onContextCalled";
21
20
  return PluginCallbackType;
22
21
  }({});
23
-
24
- //#endregion
25
- //#region ../../core/loglayer/dist/index.js
26
- const CALLBACK_LIST = [
27
- PluginCallbackType.onBeforeDataOut,
28
- PluginCallbackType.onMetadataCalled,
29
- PluginCallbackType.onBeforeMessageOut,
30
- PluginCallbackType.transformLogLevel,
31
- PluginCallbackType.shouldSendToLogger,
32
- PluginCallbackType.onContextCalled
33
- ];
22
+ PluginCallbackType.onBeforeDataOut, PluginCallbackType.onMetadataCalled, PluginCallbackType.onBeforeMessageOut, PluginCallbackType.transformLogLevel, PluginCallbackType.shouldSendToLogger, PluginCallbackType.onContextCalled;
34
23
  /**
35
24
  * The class that the mixin extends
36
25
  */
@@ -45,10 +34,48 @@ let LogLayerMixinAugmentType = /* @__PURE__ */ function(LogLayerMixinAugmentType
45
34
  LogLayerMixinAugmentType["LogLayer"] = "LogLayer";
46
35
  return LogLayerMixinAugmentType;
47
36
  }({});
48
-
49
37
  //#endregion
50
38
  //#region src/mixin.ts
51
39
  /**
40
+ * Log levels that default to rate=1 (error/fatal) — can be overridden via `perLevel` or callback.
41
+ */
42
+ const EXEMPT_LEVELS = new Set(["error", "fatal"]);
43
+ /**
44
+ * Normalize a boolean or numeric rate to a number in [0, 1].
45
+ * `true` becomes 1, `false` becomes 0, numbers are clamped to [0, 1].
46
+ * `NaN`, `Infinity`, and `-Infinity` are treated as 0 (drop all).
47
+ */
48
+ function toRate(value) {
49
+ if (value === void 0) return 1;
50
+ if (typeof value === "boolean") return value ? 1 : 0;
51
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
52
+ return Math.max(0, Math.min(1, value));
53
+ }
54
+ /**
55
+ * Run rate-based sampling (default or per_level strategy).
56
+ * Returns true if the rate check passes, false if dropped.
57
+ * Returns true for error/fatal by default, unless their rate is explicitly set in `perLevel`.
58
+ */
59
+ function runRateSampling(level, strategy, rate, perLevel) {
60
+ if (EXEMPT_LEVELS.has(level) && perLevel?.[level] === void 0) return true;
61
+ if (strategy === "per_level" && perLevel) {
62
+ const perRate = toRate(perLevel[level]);
63
+ if (perLevel[level] === void 0) {
64
+ const r = toRate(rate);
65
+ if (r === 1) return true;
66
+ if (r === 0) return false;
67
+ return Math.random() < r;
68
+ }
69
+ if (perRate === 1) return true;
70
+ if (perRate === 0) return false;
71
+ return Math.random() < perRate;
72
+ }
73
+ const r = toRate(rate);
74
+ if (r === 1) return true;
75
+ if (r === 0) return false;
76
+ return Math.random() < r;
77
+ }
78
+ /**
52
79
  * Creates a LogLayer mixin that adds wide event logging functionality.
53
80
  *
54
81
  * Wide events are comprehensive, self-contained log entries that capture an entire
@@ -60,7 +87,6 @@ let LogLayerMixinAugmentType = /* @__PURE__ */ function(LogLayerMixinAugmentType
60
87
  * wide event data across async boundaries. For Node.js, use `new AsyncLocalStorage()`
61
88
  * from `async_hooks`. For browser environments, provide your own compatible implementation.
62
89
  * @param options.includeContext - Whether to include withContext() data in emitted wide events (default: true)
63
-
64
90
  *
65
91
  * @returns A LogLayer mixin registration object
66
92
  *
@@ -95,6 +121,29 @@ function createWideEventMixin(options) {
95
121
  const asyncContext = options.asyncContext;
96
122
  const includeContext = options.includeContext ?? true;
97
123
  const wideEventField = options.wideEventField;
124
+ const errorsAsArray = options.errorsAsArray ?? false;
125
+ const errorField = options.errorField ?? (errorsAsArray ? "errors" : "error");
126
+ const snapshotPerLevel = options.sampling?.perLevel ? { ...options.sampling.perLevel } : void 0;
127
+ const samplingConfig = options.sampling ? {
128
+ strategy: options.sampling.strategy ?? "default",
129
+ rate: options.sampling.rate,
130
+ perLevel: snapshotPerLevel,
131
+ emitLevel: options.sampling.emitLevel,
132
+ shouldEmit: options.sampling.shouldEmit
133
+ } : void 0;
134
+ function defaultErrorSerializer(err) {
135
+ if (err instanceof Error) return {
136
+ name: err.name,
137
+ message: err.message,
138
+ stack: err.stack
139
+ };
140
+ return { message: String(err) };
141
+ }
142
+ function getErrorSerializer(self) {
143
+ const config = self.getConfig?.();
144
+ if (config?.errorSerializer) return config.errorSerializer;
145
+ return defaultErrorSerializer;
146
+ }
98
147
  /**
99
148
  * Context tracker plugin - stores context in async storage
100
149
  * for inclusion in wide events (only if includeContext is true)
@@ -160,18 +209,40 @@ function createWideEventMixin(options) {
160
209
  return self;
161
210
  }
162
211
  /**
212
+ * Implementation for withWideEventError
213
+ */
214
+ function withWideEventErrorImpl(error, self) {
215
+ const store = asyncContext.getStore();
216
+ if (store) {
217
+ const serializedError = getErrorSerializer(self)(error);
218
+ if (errorsAsArray) {
219
+ if (!store._llWideEvents) store._llWideEvents = {};
220
+ if (!store._llWideEvents[errorField]) store._llWideEvents[errorField] = [];
221
+ store._llWideEvents[errorField].push(serializedError);
222
+ } else withWideEventsImpl({ [errorField]: serializedError }, self);
223
+ }
224
+ return self;
225
+ }
226
+ /**
163
227
  * Implementation for emitWideEvent
164
228
  */
165
229
  function emitWideEventImpl(config, self) {
230
+ const defaultLevel = samplingConfig?.emitLevel ?? "info";
231
+ const level = config.level ?? defaultLevel;
232
+ if (samplingConfig && !samplingConfig.shouldEmit && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
166
233
  const store = asyncContext.getStore();
167
234
  const wideEventData = {};
168
235
  if (includeContext && store?._llContext) Object.assign(wideEventData, store._llContext);
169
236
  if (store?._llWideEvents) Object.assign(wideEventData, store._llWideEvents);
170
- if (config.metadata) Object.assign(wideEventData, config.metadata);
171
- const level = config.level ?? "info";
237
+ if (samplingConfig?.shouldEmit) try {
238
+ const callbackOk = samplingConfig.shouldEmit({
239
+ wideData: wideEventData,
240
+ level
241
+ });
242
+ if (!runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) || !callbackOk) return;
243
+ } catch {}
172
244
  const metadataToEmit = wideEventField ? { [wideEventField]: wideEventData } : wideEventData;
173
245
  self.withMetadata(metadataToEmit)[level](config.message);
174
- return self;
175
246
  }
176
247
  return {
177
248
  mixinsToAdd: [{
@@ -189,6 +260,9 @@ function createWideEventMixin(options) {
189
260
  prototype.emitWideEvent = function(config) {
190
261
  return emitWideEventImpl(config, this);
191
262
  };
263
+ prototype.withWideEventError = function(error) {
264
+ return withWideEventErrorImpl(error, this);
265
+ };
192
266
  },
193
267
  augmentMock: (prototype) => {
194
268
  prototype.withWideEvents = function(data) {
@@ -203,11 +277,13 @@ function createWideEventMixin(options) {
203
277
  prototype.emitWideEvent = function(config) {
204
278
  return emitWideEventImpl(config, this);
205
279
  };
280
+ prototype.withWideEventError = function(error) {
281
+ return withWideEventErrorImpl(error, this);
282
+ };
206
283
  }
207
284
  }],
208
285
  pluginsToAdd: [contextTrackerPlugin]
209
286
  };
210
287
  }
211
-
212
288
  //#endregion
213
- exports.createWideEventMixin = createWideEventMixin;
289
+ exports.createWideEventMixin = createWideEventMixin;
package/dist/index.d.cts CHANGED
@@ -2060,6 +2060,121 @@ declare class LogBuilder implements ILogBuilder<LogBuilder, boolean> {
2060
2060
  */
2061
2061
  //#endregion
2062
2062
  //#region src/types.d.ts
2063
+ /**
2064
+ * Parameters passed to a custom `shouldEmit` sampling callback.
2065
+ * Provides the accumulated wide event data and the log level so the
2066
+ * callback can make an informed keep/drop decision.
2067
+ */
2068
+ interface WideEventSamplingParams {
2069
+ /**
2070
+ * The accumulated wide event data (wide events + optional context).
2071
+ * Contains the merged data that would be emitted if kept.
2072
+ */
2073
+ wideData: Record<string, any>;
2074
+ /**
2075
+ * The log level that would be used for the emission.
2076
+ */
2077
+ level: LogLevelType;
2078
+ }
2079
+ /**
2080
+ * Sampling strategy for wide events.
2081
+ *
2082
+ * - `"default"` — a single `rate` applies to all non-error/fatal levels.
2083
+ * - `"per_level"` — per-level rates keyed by LogLevelType; levels not in the
2084
+ * map use `rate` as a fallback.
2085
+ */
2086
+ type WideEventSamplingStrategy = "default" | "per_level";
2087
+ /**
2088
+ * Configuration for sampling wide event emissions.
2089
+ *
2090
+ * "error" and "fatal" levels default to a 100% keep rate, but can be
2091
+ * overridden by setting `perLevel` rates or via `shouldEmit`.
2092
+ */
2093
+ interface WideEventSamplingConfig {
2094
+ /**
2095
+ * The sampling strategy.
2096
+ *
2097
+ * - `"default"` — a single `rate` applies to all non-error/fatal levels.
2098
+ * - `"per_level"` — per-level rates from the `perLevel` map; levels not in
2099
+ * the map use `rate` as a fallback.
2100
+ *
2101
+ * @default "default"
2102
+ */
2103
+ strategy?: WideEventSamplingStrategy;
2104
+ /**
2105
+ * A rate between 0 and 1 that determines the fraction of events to keep.
2106
+ *
2107
+ * - `1` (or `true`) — keep 100% (sampling disabled)
2108
+ * - `0.1` — ~10% of events kept
2109
+ * - `0` (or `false`) — keep 0% (all dropped for sample-able levels)
2110
+ *
2111
+ * With `"default"` strategy this rate applies to all non-error/fatal levels.
2112
+ * With `"per_level"` strategy this field is **ignored** — unmapped levels
2113
+ * default to a 100% keep rate.
2114
+ *
2115
+ * @default 1
2116
+ */
2117
+ rate?: boolean | number;
2118
+ /**
2119
+ * Per-level sampling rates when strategy is `"per_level"`.
2120
+ * Keys are log level strings (e.g. `"trace"`, `"info"`, `"warn"`).
2121
+ * Levels not listed use `rate` as a fallback.
2122
+ *
2123
+ * **Important:** "error" and "fatal" default to a 100% keep rate, but can be
2124
+ * explicitly overridden by setting their rate in `perLevel` or using a
2125
+ * `shouldEmit` callback.
2126
+ *
2127
+ * The map is snapshotted at construction time; mutating it afterward has
2128
+ * no effect.
2129
+ *
2130
+ * @example
2131
+ * ```ts
2132
+ * {
2133
+ * strategy: "per_level",
2134
+ * perLevel: {
2135
+ * trace: 0.1,
2136
+ * debug: 0.3,
2137
+ * info: 0.5,
2138
+ * },
2139
+ * }
2140
+ * // warn, error, fatal are all 100%
2141
+ * ```
2142
+ */
2143
+ perLevel?: Partial<Record<LogLevelType, boolean | number>>;
2144
+ /**
2145
+ * A custom sampling callback that receives the wide event data and log
2146
+ * level, allowing you to make an informed keep/drop decision.
2147
+ *
2148
+ * When provided, this callback is invoked **in addition to** the built-in
2149
+ * rate-based sampling. An event is only kept when **both** the built-in
2150
+ * check passes (if `rate`/`perLevel` are configured) **and** the callback
2151
+ * returns `true`. If only `shouldEmit` is set (no `rate`), the callback
2152
+ * acts as the sole gate.
2153
+ *
2154
+ * **Note:** "error" and "fatal" default to a 100% keep rate, but can be
2155
+ * overridden by returning `false` from this callback.
2156
+ *
2157
+ * @example
2158
+ * ```ts
2159
+ * {
2160
+ * shouldEmit: ({ wideData, level }) => {
2161
+ * // Only emit wide events that have a userId
2162
+ * return !!wideData.userId;
2163
+ * },
2164
+ * }
2165
+ * ```
2166
+ */
2167
+ shouldEmit?: (params: WideEventSamplingParams) => boolean;
2168
+ /**
2169
+ * Override the default log level when no explicit `level` is passed to
2170
+ * `emitWideEvent()`. When set, the resolved level uses this value instead
2171
+ * of `"info"`. This **does** affect sampling decisions — with `per_level`
2172
+ * strategy, the level from `emitLevel` determines which rate bucket applies.
2173
+ *
2174
+ * @default undefined (falls back to `"info"`)
2175
+ */
2176
+ emitLevel?: LogLevelType;
2177
+ }
2063
2178
  /**
2064
2179
  * Configuration options for creating a wide event mixin.
2065
2180
  */
@@ -2089,6 +2204,34 @@ interface WideEventMixinOptions {
2089
2204
  * { "userId": "123", "orderId": "456", "msg": "done" }
2090
2205
  */
2091
2206
  wideEventField?: string;
2207
+ /**
2208
+ * Optional: Field name to use for error data in wide events.
2209
+ * @default "error" (single mode) or "errors" (array mode)
2210
+ */
2211
+ errorField?: string;
2212
+ /**
2213
+ * Optional: When true, errors are collected as an array.
2214
+ * Each call to withWideEventError() appends to the array.
2215
+ * When false, each call replaces the previous error.
2216
+ * @default false
2217
+ */
2218
+ errorsAsArray?: boolean;
2219
+ /**
2220
+ * Optional: Sampling configuration for wide events.
2221
+ * When provided, the mixin will randomly decide whether to emit or skip wide
2222
+ * events based on the configured rate(s).
2223
+ *
2224
+ * "error" and "fatal" default to a 100% keep rate, but can be overridden
2225
+ * via `perLevel` rates or the `shouldEmit` callback.
2226
+ *
2227
+ * @example
2228
+ * // Keep ~10% of wide events (excluding errors/fatals)
2229
+ * { sampling: { strategy: "default", rate: 0.1 } }
2230
+ *
2231
+ * // Per-level: keep all trace/debug, but sample info at 5%
2232
+ * { sampling: { strategy: "per_level", perLevel: { info: 0.05 } } }
2233
+ */
2234
+ sampling?: WideEventSamplingConfig;
2092
2235
  }
2093
2236
  /**
2094
2237
  * Configuration for emitting a wide event.
@@ -2102,11 +2245,6 @@ interface EmitWideEventConfig {
2102
2245
  * Optional: Log level (defaults to "info").
2103
2246
  */
2104
2247
  level?: LogLevelType;
2105
- /**
2106
- * Optional: Additional metadata to include in this specific emission.
2107
- * This is merged with the accumulated wide event data.
2108
- */
2109
- metadata?: Record<string, any>;
2110
2248
  }
2111
2249
  /**
2112
2250
  * Interface for wide event mixin functionality.
@@ -2183,22 +2321,41 @@ interface IWideEventMixin {
2183
2321
  * and logs at the specified level.
2184
2322
  *
2185
2323
  * @param config - Configuration for the wide event emission
2186
- * @returns This logger instance for chaining
2187
2324
  *
2188
2325
  * @example
2189
2326
  * ```typescript
2190
2327
  * // Emit as info log
2191
2328
  * logger.emitWideEvent({ message: "Order processed" });
2192
2329
  *
2193
- * // Emit as error with additional metadata
2330
+ * // Emit as error
2194
2331
  * logger.emitWideEvent({
2195
2332
  * message: "Order failed",
2196
2333
  * level: "error",
2197
- * metadata: { reason: "payment_declined" }
2198
2334
  * });
2199
2335
  * ```
2200
2336
  */
2201
- emitWideEvent(config: EmitWideEventConfig): this;
2337
+ emitWideEvent(config: EmitWideEventConfig): void;
2338
+ /**
2339
+ * Captures an error for inclusion in the wide event.
2340
+ * Serializes the error using the configured errorSerializer (or default).
2341
+ *
2342
+ * @param error - The error to capture
2343
+ * @returns This logger instance for chaining
2344
+ *
2345
+ * @example
2346
+ * ```typescript
2347
+ * try {
2348
+ * await doSomething();
2349
+ * } catch (err) {
2350
+ * // For single error (replaces previous)
2351
+ * logger.withWideEventError(err);
2352
+ *
2353
+ * // With errorsAsArray: true - errors accumulate
2354
+ * logger.withWideEventError(err).withWideEventError(otherErr);
2355
+ * }
2356
+ * ```
2357
+ */
2358
+ withWideEventError(error: any): this;
2202
2359
  }
2203
2360
  declare module "loglayer" {
2204
2361
  interface LogLayer extends IWideEventMixin {}
@@ -2224,7 +2381,6 @@ declare module "@loglayer/shared" {
2224
2381
  * wide event data across async boundaries. For Node.js, use `new AsyncLocalStorage()`
2225
2382
  * from `async_hooks`. For browser environments, provide your own compatible implementation.
2226
2383
  * @param options.includeContext - Whether to include withContext() data in emitted wide events (default: true)
2227
-
2228
2384
  *
2229
2385
  * @returns A LogLayer mixin registration object
2230
2386
  *
@@ -2257,4 +2413,4 @@ declare module "@loglayer/shared" {
2257
2413
  */
2258
2414
  declare function createWideEventMixin(options: WideEventMixinOptions): LogLayerMixinRegistration;
2259
2415
  //#endregion
2260
- export { type EmitWideEventConfig, type IWideEventMixin, type WideEventMixinOptions, createWideEventMixin };
2416
+ export { type EmitWideEventConfig, type IWideEventMixin, type WideEventMixinOptions, type WideEventSamplingConfig, type WideEventSamplingParams, type WideEventSamplingStrategy, createWideEventMixin };
package/dist/index.d.mts CHANGED
@@ -2060,6 +2060,121 @@ declare class LogBuilder implements ILogBuilder<LogBuilder, boolean> {
2060
2060
  */
2061
2061
  //#endregion
2062
2062
  //#region src/types.d.ts
2063
+ /**
2064
+ * Parameters passed to a custom `shouldEmit` sampling callback.
2065
+ * Provides the accumulated wide event data and the log level so the
2066
+ * callback can make an informed keep/drop decision.
2067
+ */
2068
+ interface WideEventSamplingParams {
2069
+ /**
2070
+ * The accumulated wide event data (wide events + optional context).
2071
+ * Contains the merged data that would be emitted if kept.
2072
+ */
2073
+ wideData: Record<string, any>;
2074
+ /**
2075
+ * The log level that would be used for the emission.
2076
+ */
2077
+ level: LogLevelType;
2078
+ }
2079
+ /**
2080
+ * Sampling strategy for wide events.
2081
+ *
2082
+ * - `"default"` — a single `rate` applies to all non-error/fatal levels.
2083
+ * - `"per_level"` — per-level rates keyed by LogLevelType; levels not in the
2084
+ * map use `rate` as a fallback.
2085
+ */
2086
+ type WideEventSamplingStrategy = "default" | "per_level";
2087
+ /**
2088
+ * Configuration for sampling wide event emissions.
2089
+ *
2090
+ * "error" and "fatal" levels default to a 100% keep rate, but can be
2091
+ * overridden by setting `perLevel` rates or via `shouldEmit`.
2092
+ */
2093
+ interface WideEventSamplingConfig {
2094
+ /**
2095
+ * The sampling strategy.
2096
+ *
2097
+ * - `"default"` — a single `rate` applies to all non-error/fatal levels.
2098
+ * - `"per_level"` — per-level rates from the `perLevel` map; levels not in
2099
+ * the map use `rate` as a fallback.
2100
+ *
2101
+ * @default "default"
2102
+ */
2103
+ strategy?: WideEventSamplingStrategy;
2104
+ /**
2105
+ * A rate between 0 and 1 that determines the fraction of events to keep.
2106
+ *
2107
+ * - `1` (or `true`) — keep 100% (sampling disabled)
2108
+ * - `0.1` — ~10% of events kept
2109
+ * - `0` (or `false`) — keep 0% (all dropped for sample-able levels)
2110
+ *
2111
+ * With `"default"` strategy this rate applies to all non-error/fatal levels.
2112
+ * With `"per_level"` strategy this field is **ignored** — unmapped levels
2113
+ * default to a 100% keep rate.
2114
+ *
2115
+ * @default 1
2116
+ */
2117
+ rate?: boolean | number;
2118
+ /**
2119
+ * Per-level sampling rates when strategy is `"per_level"`.
2120
+ * Keys are log level strings (e.g. `"trace"`, `"info"`, `"warn"`).
2121
+ * Levels not listed use `rate` as a fallback.
2122
+ *
2123
+ * **Important:** "error" and "fatal" default to a 100% keep rate, but can be
2124
+ * explicitly overridden by setting their rate in `perLevel` or using a
2125
+ * `shouldEmit` callback.
2126
+ *
2127
+ * The map is snapshotted at construction time; mutating it afterward has
2128
+ * no effect.
2129
+ *
2130
+ * @example
2131
+ * ```ts
2132
+ * {
2133
+ * strategy: "per_level",
2134
+ * perLevel: {
2135
+ * trace: 0.1,
2136
+ * debug: 0.3,
2137
+ * info: 0.5,
2138
+ * },
2139
+ * }
2140
+ * // warn, error, fatal are all 100%
2141
+ * ```
2142
+ */
2143
+ perLevel?: Partial<Record<LogLevelType, boolean | number>>;
2144
+ /**
2145
+ * A custom sampling callback that receives the wide event data and log
2146
+ * level, allowing you to make an informed keep/drop decision.
2147
+ *
2148
+ * When provided, this callback is invoked **in addition to** the built-in
2149
+ * rate-based sampling. An event is only kept when **both** the built-in
2150
+ * check passes (if `rate`/`perLevel` are configured) **and** the callback
2151
+ * returns `true`. If only `shouldEmit` is set (no `rate`), the callback
2152
+ * acts as the sole gate.
2153
+ *
2154
+ * **Note:** "error" and "fatal" default to a 100% keep rate, but can be
2155
+ * overridden by returning `false` from this callback.
2156
+ *
2157
+ * @example
2158
+ * ```ts
2159
+ * {
2160
+ * shouldEmit: ({ wideData, level }) => {
2161
+ * // Only emit wide events that have a userId
2162
+ * return !!wideData.userId;
2163
+ * },
2164
+ * }
2165
+ * ```
2166
+ */
2167
+ shouldEmit?: (params: WideEventSamplingParams) => boolean;
2168
+ /**
2169
+ * Override the default log level when no explicit `level` is passed to
2170
+ * `emitWideEvent()`. When set, the resolved level uses this value instead
2171
+ * of `"info"`. This **does** affect sampling decisions — with `per_level`
2172
+ * strategy, the level from `emitLevel` determines which rate bucket applies.
2173
+ *
2174
+ * @default undefined (falls back to `"info"`)
2175
+ */
2176
+ emitLevel?: LogLevelType;
2177
+ }
2063
2178
  /**
2064
2179
  * Configuration options for creating a wide event mixin.
2065
2180
  */
@@ -2089,6 +2204,34 @@ interface WideEventMixinOptions {
2089
2204
  * { "userId": "123", "orderId": "456", "msg": "done" }
2090
2205
  */
2091
2206
  wideEventField?: string;
2207
+ /**
2208
+ * Optional: Field name to use for error data in wide events.
2209
+ * @default "error" (single mode) or "errors" (array mode)
2210
+ */
2211
+ errorField?: string;
2212
+ /**
2213
+ * Optional: When true, errors are collected as an array.
2214
+ * Each call to withWideEventError() appends to the array.
2215
+ * When false, each call replaces the previous error.
2216
+ * @default false
2217
+ */
2218
+ errorsAsArray?: boolean;
2219
+ /**
2220
+ * Optional: Sampling configuration for wide events.
2221
+ * When provided, the mixin will randomly decide whether to emit or skip wide
2222
+ * events based on the configured rate(s).
2223
+ *
2224
+ * "error" and "fatal" default to a 100% keep rate, but can be overridden
2225
+ * via `perLevel` rates or the `shouldEmit` callback.
2226
+ *
2227
+ * @example
2228
+ * // Keep ~10% of wide events (excluding errors/fatals)
2229
+ * { sampling: { strategy: "default", rate: 0.1 } }
2230
+ *
2231
+ * // Per-level: keep all trace/debug, but sample info at 5%
2232
+ * { sampling: { strategy: "per_level", perLevel: { info: 0.05 } } }
2233
+ */
2234
+ sampling?: WideEventSamplingConfig;
2092
2235
  }
2093
2236
  /**
2094
2237
  * Configuration for emitting a wide event.
@@ -2102,11 +2245,6 @@ interface EmitWideEventConfig {
2102
2245
  * Optional: Log level (defaults to "info").
2103
2246
  */
2104
2247
  level?: LogLevelType;
2105
- /**
2106
- * Optional: Additional metadata to include in this specific emission.
2107
- * This is merged with the accumulated wide event data.
2108
- */
2109
- metadata?: Record<string, any>;
2110
2248
  }
2111
2249
  /**
2112
2250
  * Interface for wide event mixin functionality.
@@ -2183,22 +2321,41 @@ interface IWideEventMixin {
2183
2321
  * and logs at the specified level.
2184
2322
  *
2185
2323
  * @param config - Configuration for the wide event emission
2186
- * @returns This logger instance for chaining
2187
2324
  *
2188
2325
  * @example
2189
2326
  * ```typescript
2190
2327
  * // Emit as info log
2191
2328
  * logger.emitWideEvent({ message: "Order processed" });
2192
2329
  *
2193
- * // Emit as error with additional metadata
2330
+ * // Emit as error
2194
2331
  * logger.emitWideEvent({
2195
2332
  * message: "Order failed",
2196
2333
  * level: "error",
2197
- * metadata: { reason: "payment_declined" }
2198
2334
  * });
2199
2335
  * ```
2200
2336
  */
2201
- emitWideEvent(config: EmitWideEventConfig): this;
2337
+ emitWideEvent(config: EmitWideEventConfig): void;
2338
+ /**
2339
+ * Captures an error for inclusion in the wide event.
2340
+ * Serializes the error using the configured errorSerializer (or default).
2341
+ *
2342
+ * @param error - The error to capture
2343
+ * @returns This logger instance for chaining
2344
+ *
2345
+ * @example
2346
+ * ```typescript
2347
+ * try {
2348
+ * await doSomething();
2349
+ * } catch (err) {
2350
+ * // For single error (replaces previous)
2351
+ * logger.withWideEventError(err);
2352
+ *
2353
+ * // With errorsAsArray: true - errors accumulate
2354
+ * logger.withWideEventError(err).withWideEventError(otherErr);
2355
+ * }
2356
+ * ```
2357
+ */
2358
+ withWideEventError(error: any): this;
2202
2359
  }
2203
2360
  declare module "loglayer" {
2204
2361
  interface LogLayer extends IWideEventMixin {}
@@ -2224,7 +2381,6 @@ declare module "@loglayer/shared" {
2224
2381
  * wide event data across async boundaries. For Node.js, use `new AsyncLocalStorage()`
2225
2382
  * from `async_hooks`. For browser environments, provide your own compatible implementation.
2226
2383
  * @param options.includeContext - Whether to include withContext() data in emitted wide events (default: true)
2227
-
2228
2384
  *
2229
2385
  * @returns A LogLayer mixin registration object
2230
2386
  *
@@ -2257,4 +2413,4 @@ declare module "@loglayer/shared" {
2257
2413
  */
2258
2414
  declare function createWideEventMixin(options: WideEventMixinOptions): LogLayerMixinRegistration;
2259
2415
  //#endregion
2260
- export { type EmitWideEventConfig, type IWideEventMixin, type WideEventMixinOptions, createWideEventMixin };
2416
+ export { type EmitWideEventConfig, type IWideEventMixin, type WideEventMixinOptions, type WideEventSamplingConfig, type WideEventSamplingParams, type WideEventSamplingStrategy, createWideEventMixin };
package/dist/index.mjs CHANGED
@@ -18,17 +18,7 @@ let PluginCallbackType = /* @__PURE__ */ function(PluginCallbackType) {
18
18
  PluginCallbackType["onContextCalled"] = "onContextCalled";
19
19
  return PluginCallbackType;
20
20
  }({});
21
-
22
- //#endregion
23
- //#region ../../core/loglayer/dist/index.js
24
- const CALLBACK_LIST = [
25
- PluginCallbackType.onBeforeDataOut,
26
- PluginCallbackType.onMetadataCalled,
27
- PluginCallbackType.onBeforeMessageOut,
28
- PluginCallbackType.transformLogLevel,
29
- PluginCallbackType.shouldSendToLogger,
30
- PluginCallbackType.onContextCalled
31
- ];
21
+ PluginCallbackType.onBeforeDataOut, PluginCallbackType.onMetadataCalled, PluginCallbackType.onBeforeMessageOut, PluginCallbackType.transformLogLevel, PluginCallbackType.shouldSendToLogger, PluginCallbackType.onContextCalled;
32
22
  /**
33
23
  * The class that the mixin extends
34
24
  */
@@ -43,10 +33,48 @@ let LogLayerMixinAugmentType = /* @__PURE__ */ function(LogLayerMixinAugmentType
43
33
  LogLayerMixinAugmentType["LogLayer"] = "LogLayer";
44
34
  return LogLayerMixinAugmentType;
45
35
  }({});
46
-
47
36
  //#endregion
48
37
  //#region src/mixin.ts
49
38
  /**
39
+ * Log levels that default to rate=1 (error/fatal) — can be overridden via `perLevel` or callback.
40
+ */
41
+ const EXEMPT_LEVELS = new Set(["error", "fatal"]);
42
+ /**
43
+ * Normalize a boolean or numeric rate to a number in [0, 1].
44
+ * `true` becomes 1, `false` becomes 0, numbers are clamped to [0, 1].
45
+ * `NaN`, `Infinity`, and `-Infinity` are treated as 0 (drop all).
46
+ */
47
+ function toRate(value) {
48
+ if (value === void 0) return 1;
49
+ if (typeof value === "boolean") return value ? 1 : 0;
50
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
51
+ return Math.max(0, Math.min(1, value));
52
+ }
53
+ /**
54
+ * Run rate-based sampling (default or per_level strategy).
55
+ * Returns true if the rate check passes, false if dropped.
56
+ * Returns true for error/fatal by default, unless their rate is explicitly set in `perLevel`.
57
+ */
58
+ function runRateSampling(level, strategy, rate, perLevel) {
59
+ if (EXEMPT_LEVELS.has(level) && perLevel?.[level] === void 0) return true;
60
+ if (strategy === "per_level" && perLevel) {
61
+ const perRate = toRate(perLevel[level]);
62
+ if (perLevel[level] === void 0) {
63
+ const r = toRate(rate);
64
+ if (r === 1) return true;
65
+ if (r === 0) return false;
66
+ return Math.random() < r;
67
+ }
68
+ if (perRate === 1) return true;
69
+ if (perRate === 0) return false;
70
+ return Math.random() < perRate;
71
+ }
72
+ const r = toRate(rate);
73
+ if (r === 1) return true;
74
+ if (r === 0) return false;
75
+ return Math.random() < r;
76
+ }
77
+ /**
50
78
  * Creates a LogLayer mixin that adds wide event logging functionality.
51
79
  *
52
80
  * Wide events are comprehensive, self-contained log entries that capture an entire
@@ -58,7 +86,6 @@ let LogLayerMixinAugmentType = /* @__PURE__ */ function(LogLayerMixinAugmentType
58
86
  * wide event data across async boundaries. For Node.js, use `new AsyncLocalStorage()`
59
87
  * from `async_hooks`. For browser environments, provide your own compatible implementation.
60
88
  * @param options.includeContext - Whether to include withContext() data in emitted wide events (default: true)
61
-
62
89
  *
63
90
  * @returns A LogLayer mixin registration object
64
91
  *
@@ -93,6 +120,29 @@ function createWideEventMixin(options) {
93
120
  const asyncContext = options.asyncContext;
94
121
  const includeContext = options.includeContext ?? true;
95
122
  const wideEventField = options.wideEventField;
123
+ const errorsAsArray = options.errorsAsArray ?? false;
124
+ const errorField = options.errorField ?? (errorsAsArray ? "errors" : "error");
125
+ const snapshotPerLevel = options.sampling?.perLevel ? { ...options.sampling.perLevel } : void 0;
126
+ const samplingConfig = options.sampling ? {
127
+ strategy: options.sampling.strategy ?? "default",
128
+ rate: options.sampling.rate,
129
+ perLevel: snapshotPerLevel,
130
+ emitLevel: options.sampling.emitLevel,
131
+ shouldEmit: options.sampling.shouldEmit
132
+ } : void 0;
133
+ function defaultErrorSerializer(err) {
134
+ if (err instanceof Error) return {
135
+ name: err.name,
136
+ message: err.message,
137
+ stack: err.stack
138
+ };
139
+ return { message: String(err) };
140
+ }
141
+ function getErrorSerializer(self) {
142
+ const config = self.getConfig?.();
143
+ if (config?.errorSerializer) return config.errorSerializer;
144
+ return defaultErrorSerializer;
145
+ }
96
146
  /**
97
147
  * Context tracker plugin - stores context in async storage
98
148
  * for inclusion in wide events (only if includeContext is true)
@@ -158,18 +208,40 @@ function createWideEventMixin(options) {
158
208
  return self;
159
209
  }
160
210
  /**
211
+ * Implementation for withWideEventError
212
+ */
213
+ function withWideEventErrorImpl(error, self) {
214
+ const store = asyncContext.getStore();
215
+ if (store) {
216
+ const serializedError = getErrorSerializer(self)(error);
217
+ if (errorsAsArray) {
218
+ if (!store._llWideEvents) store._llWideEvents = {};
219
+ if (!store._llWideEvents[errorField]) store._llWideEvents[errorField] = [];
220
+ store._llWideEvents[errorField].push(serializedError);
221
+ } else withWideEventsImpl({ [errorField]: serializedError }, self);
222
+ }
223
+ return self;
224
+ }
225
+ /**
161
226
  * Implementation for emitWideEvent
162
227
  */
163
228
  function emitWideEventImpl(config, self) {
229
+ const defaultLevel = samplingConfig?.emitLevel ?? "info";
230
+ const level = config.level ?? defaultLevel;
231
+ if (samplingConfig && !samplingConfig.shouldEmit && !runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel)) return;
164
232
  const store = asyncContext.getStore();
165
233
  const wideEventData = {};
166
234
  if (includeContext && store?._llContext) Object.assign(wideEventData, store._llContext);
167
235
  if (store?._llWideEvents) Object.assign(wideEventData, store._llWideEvents);
168
- if (config.metadata) Object.assign(wideEventData, config.metadata);
169
- const level = config.level ?? "info";
236
+ if (samplingConfig?.shouldEmit) try {
237
+ const callbackOk = samplingConfig.shouldEmit({
238
+ wideData: wideEventData,
239
+ level
240
+ });
241
+ if (!runRateSampling(level, samplingConfig.strategy, samplingConfig.rate, samplingConfig.perLevel) || !callbackOk) return;
242
+ } catch {}
170
243
  const metadataToEmit = wideEventField ? { [wideEventField]: wideEventData } : wideEventData;
171
244
  self.withMetadata(metadataToEmit)[level](config.message);
172
- return self;
173
245
  }
174
246
  return {
175
247
  mixinsToAdd: [{
@@ -187,6 +259,9 @@ function createWideEventMixin(options) {
187
259
  prototype.emitWideEvent = function(config) {
188
260
  return emitWideEventImpl(config, this);
189
261
  };
262
+ prototype.withWideEventError = function(error) {
263
+ return withWideEventErrorImpl(error, this);
264
+ };
190
265
  },
191
266
  augmentMock: (prototype) => {
192
267
  prototype.withWideEvents = function(data) {
@@ -201,11 +276,13 @@ function createWideEventMixin(options) {
201
276
  prototype.emitWideEvent = function(config) {
202
277
  return emitWideEventImpl(config, this);
203
278
  };
279
+ prototype.withWideEventError = function(error) {
280
+ return withWideEventErrorImpl(error, this);
281
+ };
204
282
  }
205
283
  }],
206
284
  pluginsToAdd: [contextTrackerPlugin]
207
285
  };
208
286
  }
209
-
210
287
  //#endregion
211
- export { createWideEventMixin };
288
+ export { createWideEventMixin };
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.0.0",
4
+ "version": "1.1.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.mjs",
@@ -32,22 +32,14 @@
32
32
  "wide-events",
33
33
  "canonical-log-lines"
34
34
  ],
35
- "scripts": {
36
- "build": "tsdown src/index.ts",
37
- "test": "vitest --run",
38
- "clean": "rm -rf .turbo node_modules dist",
39
- "lint": "biome check --no-errors-on-unmatched --write --unsafe src",
40
- "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
41
- "verify-types": "tsc --noEmit"
42
- },
43
35
  "devDependencies": {
44
- "@internal/tsconfig": "workspace:*",
45
- "@loglayer/shared": "workspace:*",
46
- "@types/node": "25.2.3",
47
- "loglayer": "workspace:*",
48
- "tsdown": "0.20.3",
49
- "typescript": "5.9.3",
50
- "vitest": "4.0.18"
36
+ "@types/node": "25.9.1",
37
+ "tsdown": "0.22.0",
38
+ "typescript": "6.0.3",
39
+ "vitest": "4.1.7",
40
+ "@loglayer/shared": "4.2.0",
41
+ "loglayer": "9.2.0",
42
+ "@internal/tsconfig": "2.1.0"
51
43
  },
52
44
  "peerDependencies": {},
53
45
  "bugs": "https://github.com/loglayer/loglayer/issues",
@@ -55,5 +47,12 @@
55
47
  "dist"
56
48
  ],
57
49
  "homepage": "https://loglayer.dev",
58
- "packageManager": "pnpm@10.20.0"
59
- }
50
+ "scripts": {
51
+ "build": "tsdown src/index.ts",
52
+ "test": "vitest --run",
53
+ "clean": "rm -rf .turbo node_modules dist",
54
+ "lint": "biome check --no-errors-on-unmatched --write --unsafe src",
55
+ "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
56
+ "verify-types": "tsc --noEmit"
57
+ }
58
+ }