@forinda/kickjs-otel 3.1.1 → 3.1.2

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.d.mts CHANGED
@@ -22,6 +22,34 @@ interface OtelAdapterOptions {
22
22
  * @example ['/health', '/_debug/*', '/favicon.ico']
23
23
  */
24
24
  ignoreRoutes?: string[];
25
+ /**
26
+ * Span-attribute keys to mask before export. Mirrors pino's
27
+ * `redact.paths` contract so one list can drive both log and span
28
+ * redaction:
29
+ *
30
+ * ```ts
31
+ * import { sensitiveKeys } from './config/redaction'
32
+ *
33
+ * pino({ redact: { paths: sensitiveKeys } })
34
+ * new OtelAdapter({ sensitiveKeys })
35
+ * ```
36
+ *
37
+ * String entries do case-insensitive exact match on the attribute
38
+ * key (not value); `RegExp` entries are matched against the key.
39
+ * Matching attributes have their value replaced with `'[REDACTED]'`.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * sensitiveKeys: ['password', 'token', /^x-api-key/i, /authorization/i]
44
+ * ```
45
+ */
46
+ sensitiveKeys?: (string | RegExp)[];
47
+ /**
48
+ * Custom redactor — takes precedence over `sensitiveKeys` when set.
49
+ * Return the replacement value (`'[REDACTED]'` by convention) or the
50
+ * original value to let it through.
51
+ */
52
+ redactAttribute?: (key: string, value: unknown) => unknown;
25
53
  }
26
54
  //#endregion
27
55
  //#region src/otel.adapter.d.ts
@@ -58,11 +86,18 @@ declare class OtelAdapter implements AppAdapter {
58
86
  private meter;
59
87
  private requestCounter;
60
88
  private requestDuration;
89
+ private readonly redact;
61
90
  constructor(options?: OtelAdapterOptions);
62
91
  beforeStart({}: AdapterContext): void;
63
92
  middleware(): AdapterMiddleware[];
64
93
  private onFinish;
65
94
  private shouldIgnore;
95
+ /**
96
+ * Run the configured redactor over an attribute bag. Exposed so
97
+ * downstream code that adds attributes directly on a span
98
+ * (`span.setAttributes(...)`) can share the same redaction contract.
99
+ */
100
+ applyRedaction<T extends Record<string, unknown>>(attrs: T): Record<string, unknown>;
66
101
  shutdown(): Promise<void>;
67
102
  }
68
103
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/otel.adapter.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,WAAA;EAFe;EAKf,cAAA;;EAGA,OAAA;EANA;EASA,OAAA;EAHA;;;;EASA,gBAAA,IAAoB,GAAA,UAAa,MAAA;EAOjC;;;;;EAAA,YAAA;AAAA;;;;AAxBF;;;;;;;;;;;;;;;;;ACqCA;;;;;;;;cAAa,WAAA,YAAuB,UAAA;EAClC,IAAA;EAAA,QACQ,OAAA;EAAA,QAIA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,cAAA;EAAA,QACA,eAAA;cAEI,OAAA,GAAS,kBAAA;EAUrB,WAAA,CAAA,IAAgB,cAAA;EA+BhB,UAAA,CAAA,GAAc,iBAAA;EAAA,QA2CN,QAAA;EAAA,QAgCA,YAAA;EAUF,QAAA,CAAA,GAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/otel.adapter.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,WAAA;EAFe;EAKf,cAAA;;EAGA,OAAA;EANA;EASA,OAAA;EAHA;;;;EASA,gBAAA,IAAoB,GAAA,UAAa,MAAA;EAOjC;;;;;EAAA,YAAA;EA8B8C;;;;;ACjBhD;;;;;;;;;;;;;;;;EDUE,aAAA,aAA0B,MAAA;ECHlB;;;;;EDUR,eAAA,IAAmB,GAAA,UAAa,KAAA;AAAA;;;;AAtDlC;;;;;;;;;;;;;;;;;;;;;;ACqCA;;;cAAa,WAAA,YAAuB,UAAA;EAClC,IAAA;EAAA,QACQ,OAAA;EAAA,QAIA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,cAAA;EAAA,QACA,eAAA;EAAA,iBACS,MAAA;cAEL,OAAA,GAAS,kBAAA;EAWrB,WAAA,CAAA,IAAgB,cAAA;EA+BhB,UAAA,CAAA,GAAc,iBAAA;EAAA,QA2CN,QAAA;EAAA,QAkCA,YAAA;EA7HA;;;;;EA4IR,cAAA,WAAyB,MAAA,kBAAA,CAAyB,KAAA,EAAO,CAAA,GAAI,MAAA;EAQvD,QAAA,CAAA,GAAY,OAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-otel v3.1.1
2
+ * @forinda/kickjs-otel v3.1.2
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -48,6 +48,7 @@ var OtelAdapter = class {
48
48
  meter = null;
49
49
  requestCounter = null;
50
50
  requestDuration = null;
51
+ redact;
51
52
  constructor(options = {}) {
52
53
  this.options = {
53
54
  serviceName: options.serviceName ?? "kickjs-app",
@@ -56,6 +57,7 @@ var OtelAdapter = class {
56
57
  metrics: options.metrics ?? true,
57
58
  ...options
58
59
  };
60
+ this.redact = buildRedactor(this.options.sensitiveKeys, this.options.redactAttribute);
59
61
  }
60
62
  beforeStart({}) {
61
63
  try {
@@ -85,14 +87,14 @@ var OtelAdapter = class {
85
87
  let span = null;
86
88
  if (this.tracer) {
87
89
  const otelApi = __require("@opentelemetry/api");
88
- span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, { attributes: {
90
+ span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, { attributes: this.applyRedaction({
89
91
  "http.method": req.method,
90
92
  "http.url": req.originalUrl,
91
93
  "http.target": req.path,
92
94
  "http.user_agent": req.get("user-agent") ?? "",
93
95
  "net.host.name": req.hostname,
94
96
  ...this.options.customAttributes?.(req) ?? {}
95
- } });
97
+ }) });
96
98
  const ctx = otelApi.trace.setSpan(otelApi.context.active(), span);
97
99
  otelApi.context.with(ctx, () => {
98
100
  this.onFinish(req, res, startTime, span);
@@ -116,10 +118,10 @@ var OtelAdapter = class {
116
118
  "http.status_code": res.statusCode
117
119
  };
118
120
  if (span) {
119
- span.setAttributes({
121
+ span.setAttributes(this.applyRedaction({
120
122
  "http.status_code": res.statusCode,
121
123
  "http.route": route
122
- });
124
+ }));
123
125
  if (res.statusCode >= 400) span.setStatus({
124
126
  code: 2,
125
127
  message: `HTTP ${res.statusCode}`
@@ -137,10 +139,41 @@ var OtelAdapter = class {
137
139
  return path === pattern;
138
140
  });
139
141
  }
142
+ /**
143
+ * Run the configured redactor over an attribute bag. Exposed so
144
+ * downstream code that adds attributes directly on a span
145
+ * (`span.setAttributes(...)`) can share the same redaction contract.
146
+ */
147
+ applyRedaction(attrs) {
148
+ const out = {};
149
+ for (const key of Object.keys(attrs)) out[key] = this.redact(key, attrs[key]);
150
+ return out;
151
+ }
140
152
  async shutdown() {
141
153
  log.info("OTel adapter shutdown");
142
154
  }
143
155
  };
156
+ /**
157
+ * Build an attribute redactor from `sensitiveKeys` + optional
158
+ * `redactAttribute` override. String keys match case-insensitively
159
+ * against the attribute name; `RegExp` entries are matched verbatim.
160
+ * A custom `redactAttribute` runs after the key-based mask so users
161
+ * can inspect values too.
162
+ */
163
+ function buildRedactor(sensitiveKeys, custom) {
164
+ if (custom) return custom;
165
+ if (!sensitiveKeys || sensitiveKeys.length === 0) return (_k, v) => v;
166
+ const lowered = /* @__PURE__ */ new Set();
167
+ const patterns = [];
168
+ for (const entry of sensitiveKeys) if (typeof entry === "string") lowered.add(entry.toLowerCase());
169
+ else patterns.push(entry);
170
+ return (key, value) => {
171
+ const lower = key.toLowerCase();
172
+ if (lowered.has(lower)) return "[REDACTED]";
173
+ for (const p of patterns) if (p.test(key)) return "[REDACTED]";
174
+ return value;
175
+ };
176
+ }
144
177
  //#endregion
145
178
  export { OtelAdapter };
146
179
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/otel.adapter.ts"],"sourcesContent":["import {\n Logger,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n} from '@forinda/kickjs'\nimport type { Request, Response, NextFunction } from 'express'\nimport type { OtelAdapterOptions } from './types'\n\nconst log = Logger.for('OtelAdapter')\n\n/**\n * OpenTelemetry adapter for KickJS — automatic tracing and metrics.\n *\n * Creates spans for each HTTP request with route, method, status code,\n * and duration. Optionally records request count and latency histograms.\n *\n * Works with any OTel-compatible backend: Jaeger, Grafana Tempo, Datadog,\n * Honeycomb, etc. Configure exporters via the OTel SDK before bootstrapping.\n *\n * @example\n * ```ts\n * import { OtelAdapter } from '@forinda/kickjs-otel'\n *\n * // Set up OTel SDK (e.g., with Jaeger exporter) before bootstrap\n * bootstrap({\n * modules,\n * adapters: [\n * new OtelAdapter({\n * serviceName: 'my-api',\n * serviceVersion: '1.0.0',\n * ignoreRoutes: ['/health', '/_debug/*'],\n * }),\n * ],\n * })\n * ```\n */\nexport class OtelAdapter implements AppAdapter {\n name = 'OtelAdapter'\n private options: Required<\n Pick<OtelAdapterOptions, 'serviceName' | 'serviceVersion' | 'tracing' | 'metrics'>\n > &\n OtelAdapterOptions\n private tracer: any = null\n private meter: any = null\n private requestCounter: any = null\n private requestDuration: any = null\n\n constructor(options: OtelAdapterOptions = {}) {\n this.options = {\n serviceName: options.serviceName ?? 'kickjs-app',\n serviceVersion: options.serviceVersion ?? '0.0.0',\n tracing: options.tracing ?? true,\n metrics: options.metrics ?? true,\n ...options,\n }\n }\n\n beforeStart({}: AdapterContext): void {\n try {\n // Dynamically import OTel API — it's a peer dependency\n const otelApi = require('@opentelemetry/api')\n\n if (this.options.tracing) {\n this.tracer = otelApi.trace.getTracer(this.options.serviceName, this.options.serviceVersion)\n log.info(`Tracing enabled for ${this.options.serviceName}`)\n }\n\n if (this.options.metrics) {\n this.meter = otelApi.metrics.getMeter(this.options.serviceName, this.options.serviceVersion)\n\n this.requestCounter = this.meter.createCounter('http.server.request.count', {\n description: 'Total number of HTTP requests',\n })\n\n this.requestDuration = this.meter.createHistogram('http.server.request.duration', {\n description: 'HTTP request duration in milliseconds',\n unit: 'ms',\n })\n\n log.info('Metrics enabled — http.server.request.count, http.server.request.duration')\n }\n } catch {\n log.warn(\n 'OpenTelemetry API not found. Install @opentelemetry/api to enable tracing and metrics.',\n )\n }\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n // Skip ignored routes\n if (this.shouldIgnore(req.path)) {\n return next()\n }\n\n const startTime = performance.now()\n\n // Start a span if tracing is enabled\n let span: any = null\n if (this.tracer) {\n const otelApi = require('@opentelemetry/api')\n span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, {\n attributes: {\n 'http.method': req.method,\n 'http.url': req.originalUrl,\n 'http.target': req.path,\n 'http.user_agent': req.get('user-agent') ?? '',\n 'net.host.name': req.hostname,\n ...(this.options.customAttributes?.(req) ?? {}),\n },\n })\n\n // Set span on context so downstream code can add attributes\n const ctx = otelApi.trace.setSpan(otelApi.context.active(), span)\n otelApi.context.with(ctx, () => {\n this.onFinish(req, res, startTime, span)\n next()\n })\n return\n }\n\n this.onFinish(req, res, startTime, null)\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n private onFinish(req: Request, res: Response, startTime: number, span: any): void {\n res.on('finish', () => {\n const duration = performance.now() - startTime\n const route = (req as any).route?.path ?? req.path\n const attributes = {\n 'http.method': req.method,\n 'http.route': route,\n 'http.status_code': res.statusCode,\n }\n\n // End span\n if (span) {\n span.setAttributes({\n 'http.status_code': res.statusCode,\n 'http.route': route,\n })\n if (res.statusCode >= 400) {\n span.setStatus({ code: 2, message: `HTTP ${res.statusCode}` })\n }\n span.end()\n }\n\n // Record metrics\n if (this.requestCounter) {\n this.requestCounter.add(1, attributes)\n }\n if (this.requestDuration) {\n this.requestDuration.record(duration, attributes)\n }\n })\n }\n\n private shouldIgnore(path: string): boolean {\n if (!this.options.ignoreRoutes) return false\n return this.options.ignoreRoutes.some((pattern) => {\n if (pattern.endsWith('*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return path === pattern\n })\n }\n\n async shutdown(): Promise<void> {\n log.info('OTel adapter shutdown')\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/otel.adapter.ts"],"sourcesContent":["import {\n Logger,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n} from '@forinda/kickjs'\nimport type { Request, Response, NextFunction } from 'express'\nimport type { OtelAdapterOptions } from './types'\n\nconst log = Logger.for('OtelAdapter')\n\n/**\n * OpenTelemetry adapter for KickJS — automatic tracing and metrics.\n *\n * Creates spans for each HTTP request with route, method, status code,\n * and duration. Optionally records request count and latency histograms.\n *\n * Works with any OTel-compatible backend: Jaeger, Grafana Tempo, Datadog,\n * Honeycomb, etc. Configure exporters via the OTel SDK before bootstrapping.\n *\n * @example\n * ```ts\n * import { OtelAdapter } from '@forinda/kickjs-otel'\n *\n * // Set up OTel SDK (e.g., with Jaeger exporter) before bootstrap\n * bootstrap({\n * modules,\n * adapters: [\n * new OtelAdapter({\n * serviceName: 'my-api',\n * serviceVersion: '1.0.0',\n * ignoreRoutes: ['/health', '/_debug/*'],\n * }),\n * ],\n * })\n * ```\n */\nexport class OtelAdapter implements AppAdapter {\n name = 'OtelAdapter'\n private options: Required<\n Pick<OtelAdapterOptions, 'serviceName' | 'serviceVersion' | 'tracing' | 'metrics'>\n > &\n OtelAdapterOptions\n private tracer: any = null\n private meter: any = null\n private requestCounter: any = null\n private requestDuration: any = null\n private readonly redact: (key: string, value: unknown) => unknown\n\n constructor(options: OtelAdapterOptions = {}) {\n this.options = {\n serviceName: options.serviceName ?? 'kickjs-app',\n serviceVersion: options.serviceVersion ?? '0.0.0',\n tracing: options.tracing ?? true,\n metrics: options.metrics ?? true,\n ...options,\n }\n this.redact = buildRedactor(this.options.sensitiveKeys, this.options.redactAttribute)\n }\n\n beforeStart({}: AdapterContext): void {\n try {\n // Dynamically import OTel API — it's a peer dependency\n const otelApi = require('@opentelemetry/api')\n\n if (this.options.tracing) {\n this.tracer = otelApi.trace.getTracer(this.options.serviceName, this.options.serviceVersion)\n log.info(`Tracing enabled for ${this.options.serviceName}`)\n }\n\n if (this.options.metrics) {\n this.meter = otelApi.metrics.getMeter(this.options.serviceName, this.options.serviceVersion)\n\n this.requestCounter = this.meter.createCounter('http.server.request.count', {\n description: 'Total number of HTTP requests',\n })\n\n this.requestDuration = this.meter.createHistogram('http.server.request.duration', {\n description: 'HTTP request duration in milliseconds',\n unit: 'ms',\n })\n\n log.info('Metrics enabled — http.server.request.count, http.server.request.duration')\n }\n } catch {\n log.warn(\n 'OpenTelemetry API not found. Install @opentelemetry/api to enable tracing and metrics.',\n )\n }\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n // Skip ignored routes\n if (this.shouldIgnore(req.path)) {\n return next()\n }\n\n const startTime = performance.now()\n\n // Start a span if tracing is enabled\n let span: any = null\n if (this.tracer) {\n const otelApi = require('@opentelemetry/api')\n span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, {\n attributes: this.applyRedaction({\n 'http.method': req.method,\n 'http.url': req.originalUrl,\n 'http.target': req.path,\n 'http.user_agent': req.get('user-agent') ?? '',\n 'net.host.name': req.hostname,\n ...(this.options.customAttributes?.(req) ?? {}),\n }),\n })\n\n // Set span on context so downstream code can add attributes\n const ctx = otelApi.trace.setSpan(otelApi.context.active(), span)\n otelApi.context.with(ctx, () => {\n this.onFinish(req, res, startTime, span)\n next()\n })\n return\n }\n\n this.onFinish(req, res, startTime, null)\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n private onFinish(req: Request, res: Response, startTime: number, span: any): void {\n res.on('finish', () => {\n const duration = performance.now() - startTime\n const route = (req as any).route?.path ?? req.path\n const attributes = {\n 'http.method': req.method,\n 'http.route': route,\n 'http.status_code': res.statusCode,\n }\n\n // End span\n if (span) {\n span.setAttributes(\n this.applyRedaction({\n 'http.status_code': res.statusCode,\n 'http.route': route,\n }),\n )\n if (res.statusCode >= 400) {\n span.setStatus({ code: 2, message: `HTTP ${res.statusCode}` })\n }\n span.end()\n }\n\n // Record metrics\n if (this.requestCounter) {\n this.requestCounter.add(1, attributes)\n }\n if (this.requestDuration) {\n this.requestDuration.record(duration, attributes)\n }\n })\n }\n\n private shouldIgnore(path: string): boolean {\n if (!this.options.ignoreRoutes) return false\n return this.options.ignoreRoutes.some((pattern) => {\n if (pattern.endsWith('*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return path === pattern\n })\n }\n\n /**\n * Run the configured redactor over an attribute bag. Exposed so\n * downstream code that adds attributes directly on a span\n * (`span.setAttributes(...)`) can share the same redaction contract.\n */\n applyRedaction<T extends Record<string, unknown>>(attrs: T): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(attrs)) {\n out[key] = this.redact(key, attrs[key])\n }\n return out\n }\n\n async shutdown(): Promise<void> {\n log.info('OTel adapter shutdown')\n }\n}\n\n/**\n * Build an attribute redactor from `sensitiveKeys` + optional\n * `redactAttribute` override. String keys match case-insensitively\n * against the attribute name; `RegExp` entries are matched verbatim.\n * A custom `redactAttribute` runs after the key-based mask so users\n * can inspect values too.\n */\nfunction buildRedactor(\n sensitiveKeys: (string | RegExp)[] | undefined,\n custom: ((key: string, value: unknown) => unknown) | undefined,\n): (key: string, value: unknown) => unknown {\n if (custom) return custom\n if (!sensitiveKeys || sensitiveKeys.length === 0) return (_k, v) => v\n\n const lowered = new Set<string>()\n const patterns: RegExp[] = []\n for (const entry of sensitiveKeys) {\n if (typeof entry === 'string') lowered.add(entry.toLowerCase())\n else patterns.push(entry)\n }\n\n return (key: string, value: unknown) => {\n const lower = key.toLowerCase()\n if (lowered.has(lower)) return '[REDACTED]'\n for (const p of patterns) {\n if (p.test(key)) return '[REDACTED]'\n }\n return value\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AASA,MAAM,MAAM,OAAO,IAAI,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BrC,IAAa,cAAb,MAA+C;CAC7C,OAAO;CACP;CAIA,SAAsB;CACtB,QAAqB;CACrB,iBAA8B;CAC9B,kBAA+B;CAC/B;CAEA,YAAY,UAA8B,EAAE,EAAE;AAC5C,OAAK,UAAU;GACb,aAAa,QAAQ,eAAe;GACpC,gBAAgB,QAAQ,kBAAkB;GAC1C,SAAS,QAAQ,WAAW;GAC5B,SAAS,QAAQ,WAAW;GAC5B,GAAG;GACJ;AACD,OAAK,SAAS,cAAc,KAAK,QAAQ,eAAe,KAAK,QAAQ,gBAAgB;;CAGvF,YAAY,IAA0B;AACpC,MAAI;GAEF,MAAM,UAAA,UAAkB,qBAAqB;AAE7C,OAAI,KAAK,QAAQ,SAAS;AACxB,SAAK,SAAS,QAAQ,MAAM,UAAU,KAAK,QAAQ,aAAa,KAAK,QAAQ,eAAe;AAC5F,QAAI,KAAK,uBAAuB,KAAK,QAAQ,cAAc;;AAG7D,OAAI,KAAK,QAAQ,SAAS;AACxB,SAAK,QAAQ,QAAQ,QAAQ,SAAS,KAAK,QAAQ,aAAa,KAAK,QAAQ,eAAe;AAE5F,SAAK,iBAAiB,KAAK,MAAM,cAAc,6BAA6B,EAC1E,aAAa,iCACd,CAAC;AAEF,SAAK,kBAAkB,KAAK,MAAM,gBAAgB,gCAAgC;KAChF,aAAa;KACb,MAAM;KACP,CAAC;AAEF,QAAI,KAAK,4EAA4E;;UAEjF;AACN,OAAI,KACF,yFACD;;;CAIL,aAAkC;AAChC,SAAO,CACL;GACE,UAAU,KAAc,KAAe,SAAuB;AAE5D,QAAI,KAAK,aAAa,IAAI,KAAK,CAC7B,QAAO,MAAM;IAGf,MAAM,YAAY,YAAY,KAAK;IAGnC,IAAI,OAAY;AAChB,QAAI,KAAK,QAAQ;KACf,MAAM,UAAA,UAAkB,qBAAqB;AAC7C,YAAO,KAAK,OAAO,UAAU,GAAG,IAAI,OAAO,GAAG,IAAI,OAAO,QAAQ,IAAI,QAAQ,EAC3E,YAAY,KAAK,eAAe;MAC9B,eAAe,IAAI;MACnB,YAAY,IAAI;MAChB,eAAe,IAAI;MACnB,mBAAmB,IAAI,IAAI,aAAa,IAAI;MAC5C,iBAAiB,IAAI;MACrB,GAAI,KAAK,QAAQ,mBAAmB,IAAI,IAAI,EAAE;MAC/C,CAAC,EACH,CAAC;KAGF,MAAM,MAAM,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,EAAE,KAAK;AACjE,aAAQ,QAAQ,KAAK,WAAW;AAC9B,WAAK,SAAS,KAAK,KAAK,WAAW,KAAK;AACxC,YAAM;OACN;AACF;;AAGF,SAAK,SAAS,KAAK,KAAK,WAAW,KAAK;AACxC,UAAM;;GAER,OAAO;GACR,CACF;;CAGH,SAAiB,KAAc,KAAe,WAAmB,MAAiB;AAChF,MAAI,GAAG,gBAAgB;GACrB,MAAM,WAAW,YAAY,KAAK,GAAG;GACrC,MAAM,QAAS,IAAY,OAAO,QAAQ,IAAI;GAC9C,MAAM,aAAa;IACjB,eAAe,IAAI;IACnB,cAAc;IACd,oBAAoB,IAAI;IACzB;AAGD,OAAI,MAAM;AACR,SAAK,cACH,KAAK,eAAe;KAClB,oBAAoB,IAAI;KACxB,cAAc;KACf,CAAC,CACH;AACD,QAAI,IAAI,cAAc,IACpB,MAAK,UAAU;KAAE,MAAM;KAAG,SAAS,QAAQ,IAAI;KAAc,CAAC;AAEhE,SAAK,KAAK;;AAIZ,OAAI,KAAK,eACP,MAAK,eAAe,IAAI,GAAG,WAAW;AAExC,OAAI,KAAK,gBACP,MAAK,gBAAgB,OAAO,UAAU,WAAW;IAEnD;;CAGJ,aAAqB,MAAuB;AAC1C,MAAI,CAAC,KAAK,QAAQ,aAAc,QAAO;AACvC,SAAO,KAAK,QAAQ,aAAa,MAAM,YAAY;AACjD,OAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;AAE9C,UAAO,SAAS;IAChB;;;;;;;CAQJ,eAAkD,OAAmC;EACnF,MAAM,MAA+B,EAAE;AACvC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,KAAI,OAAO,KAAK,OAAO,KAAK,MAAM,KAAK;AAEzC,SAAO;;CAGT,MAAM,WAA0B;AAC9B,MAAI,KAAK,wBAAwB;;;;;;;;;;AAWrC,SAAS,cACP,eACA,QAC0C;AAC1C,KAAI,OAAQ,QAAO;AACnB,KAAI,CAAC,iBAAiB,cAAc,WAAW,EAAG,SAAQ,IAAI,MAAM;CAEpE,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,SAAS,cAClB,KAAI,OAAO,UAAU,SAAU,SAAQ,IAAI,MAAM,aAAa,CAAC;KAC1D,UAAS,KAAK,MAAM;AAG3B,SAAQ,KAAa,UAAmB;EACtC,MAAM,QAAQ,IAAI,aAAa;AAC/B,MAAI,QAAQ,IAAI,MAAM,CAAE,QAAO;AAC/B,OAAK,MAAM,KAAK,SACd,KAAI,EAAE,KAAK,IAAI,CAAE,QAAO;AAE1B,SAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-otel",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "OpenTelemetry adapter for KickJS — automatic tracing, metrics, and export to any OTel backend",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -101,7 +101,9 @@
101
101
  "@opentelemetry/semantic-conventions": "^1.4.0",
102
102
  "@types/node": "^25.0.0",
103
103
  "typescript": "^5.9.2",
104
- "@forinda/kickjs": "3.1.1"
104
+ "unplugin-swc": "^1.5.9",
105
+ "vitest": "^4.1.2",
106
+ "@forinda/kickjs": "3.1.2"
105
107
  },
106
108
  "publishConfig": {
107
109
  "access": "public"
@@ -123,6 +125,7 @@
123
125
  "scripts": {
124
126
  "build": "wireit",
125
127
  "dev": "tsdown --watch",
128
+ "test": "vitest run",
126
129
  "typecheck": "tsc --noEmit",
127
130
  "clean": "rm -rf dist .wireit",
128
131
  "lint": "tsc --noEmit"