evlog 1.1.0 → 1.2.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/error.d.mts CHANGED
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.mjs';
16
16
  * ```
17
17
  */
18
18
  declare class EvlogError extends Error {
19
+ /** HTTP status code */
19
20
  readonly status: number;
20
21
  readonly why?: string;
21
22
  readonly fix?: string;
22
23
  readonly link?: string;
23
24
  constructor(options: ErrorOptions | string);
25
+ /** HTTP status text (alias for message) */
26
+ get statusText(): string;
27
+ /** HTTP status code (alias for compatibility) */
24
28
  get statusCode(): number;
29
+ /** HTTP status message (alias for compatibility) */
30
+ get statusMessage(): string;
31
+ /** Structured data for serialization */
25
32
  get data(): {
26
- why: string | undefined;
27
- fix: string | undefined;
28
- link: string | undefined;
33
+ why?: string;
34
+ fix?: string;
35
+ link?: string;
29
36
  } | undefined;
30
37
  toString(): string;
31
38
  toJSON(): Record<string, unknown>;
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
34
41
  * Create a structured error with context for debugging and user-facing messages.
35
42
  *
36
43
  * @param options - Error message string or full options object
37
- * @returns EvlogError instance compatible with Nitro's error handling
44
+ * @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
38
45
  *
39
46
  * @example
40
47
  * ```ts
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
52
59
  * ```
53
60
  */
54
61
  declare function createError(options: ErrorOptions | string): EvlogError;
55
- declare const createEvlogError: typeof createError;
56
62
 
57
- export { EvlogError, createError, createEvlogError };
63
+ export { EvlogError, createError, createError as createEvlogError };
package/dist/error.d.ts CHANGED
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.js';
16
16
  * ```
17
17
  */
18
18
  declare class EvlogError extends Error {
19
+ /** HTTP status code */
19
20
  readonly status: number;
20
21
  readonly why?: string;
21
22
  readonly fix?: string;
22
23
  readonly link?: string;
23
24
  constructor(options: ErrorOptions | string);
25
+ /** HTTP status text (alias for message) */
26
+ get statusText(): string;
27
+ /** HTTP status code (alias for compatibility) */
24
28
  get statusCode(): number;
29
+ /** HTTP status message (alias for compatibility) */
30
+ get statusMessage(): string;
31
+ /** Structured data for serialization */
25
32
  get data(): {
26
- why: string | undefined;
27
- fix: string | undefined;
28
- link: string | undefined;
33
+ why?: string;
34
+ fix?: string;
35
+ link?: string;
29
36
  } | undefined;
30
37
  toString(): string;
31
38
  toJSON(): Record<string, unknown>;
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
34
41
  * Create a structured error with context for debugging and user-facing messages.
35
42
  *
36
43
  * @param options - Error message string or full options object
37
- * @returns EvlogError instance compatible with Nitro's error handling
44
+ * @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
38
45
  *
39
46
  * @example
40
47
  * ```ts
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
52
59
  * ```
53
60
  */
54
61
  declare function createError(options: ErrorOptions | string): EvlogError;
55
- declare const createEvlogError: typeof createError;
56
62
 
57
- export { EvlogError, createError, createEvlogError };
63
+ export { EvlogError, createError, createError as createEvlogError };
package/dist/error.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { colors, isServer } from './utils.mjs';
2
2
 
3
3
  class EvlogError extends Error {
4
+ /** HTTP status code */
4
5
  status;
5
6
  why;
6
7
  fix;
@@ -17,11 +18,24 @@ class EvlogError extends Error {
17
18
  Error.captureStackTrace(this, EvlogError);
18
19
  }
19
20
  }
21
+ /** HTTP status text (alias for message) */
22
+ get statusText() {
23
+ return this.message;
24
+ }
25
+ /** HTTP status code (alias for compatibility) */
20
26
  get statusCode() {
21
27
  return this.status;
22
28
  }
29
+ /** HTTP status message (alias for compatibility) */
30
+ get statusMessage() {
31
+ return this.message;
32
+ }
33
+ /** Structured data for serialization */
23
34
  get data() {
24
- return this.why || this.fix || this.link ? { why: this.why, fix: this.fix, link: this.link } : void 0;
35
+ if (this.why || this.fix || this.link) {
36
+ return { why: this.why, fix: this.fix, link: this.link };
37
+ }
38
+ return void 0;
25
39
  }
26
40
  toString() {
27
41
  const useColors = isServer();
@@ -48,21 +62,20 @@ class EvlogError extends Error {
48
62
  return lines.join("\n");
49
63
  }
50
64
  toJSON() {
65
+ const { data } = this;
51
66
  return {
52
67
  name: this.name,
53
68
  message: this.message,
54
69
  status: this.status,
55
- why: this.why,
56
- fix: this.fix,
57
- link: this.link,
58
- cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : void 0,
59
- stack: this.stack
70
+ ...data && { data },
71
+ ...this.cause instanceof Error && {
72
+ cause: { name: this.cause.name, message: this.cause.message }
73
+ }
60
74
  };
61
75
  }
62
76
  }
63
77
  function createError(options) {
64
78
  return new EvlogError(options);
65
79
  }
66
- const createEvlogError = createError;
67
80
 
68
- export { EvlogError, createError, createEvlogError };
81
+ export { EvlogError, createError, createError as createEvlogError };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- export { EvlogError, createError, createEvlogError } from './error.mjs';
1
+ export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.mjs';
3
3
  export { useLogger } from './runtime/server/useLogger.mjs';
4
4
  export { parseError } from './runtime/utils/parseError.mjs';
5
- export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.mjs';
5
+ export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.mjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { EvlogError, createError, createEvlogError } from './error.js';
1
+ export { EvlogError, createError, createError as createEvlogError } from './error.js';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.js';
3
3
  export { useLogger } from './runtime/server/useLogger.js';
4
4
  export { parseError } from './runtime/utils/parseError.js';
5
- export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.js';
5
+ export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.js';
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- export { EvlogError, createError, createEvlogError } from './error.mjs';
1
+ export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.mjs';
3
3
  export { useLogger } from './runtime/server/useLogger.mjs';
4
4
  export { parseError } from './runtime/utils/parseError.mjs';
5
5
  import './utils.mjs';
6
+ import 'defu';
package/dist/logger.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { defu } from 'defu';
1
2
  import { isDev, detectEnvironment, formatDuration, matchesPattern, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
2
3
 
3
4
  let globalEnv = {
@@ -149,13 +150,12 @@ function createRequestLogger(options = {}) {
149
150
  let hasError = false;
150
151
  return {
151
152
  set(data) {
152
- context = { ...context, ...data };
153
+ context = defu(data, context);
153
154
  },
154
155
  error(error, errorContext) {
155
156
  hasError = true;
156
157
  const err = typeof error === "string" ? new Error(error) : error;
157
- context = {
158
- ...context,
158
+ const errorData = {
159
159
  ...errorContext,
160
160
  error: {
161
161
  name: err.name,
@@ -163,6 +163,7 @@ function createRequestLogger(options = {}) {
163
163
  stack: err.stack
164
164
  }
165
165
  };
166
+ context = defu(errorData, context);
166
167
  },
167
168
  emit(overrides) {
168
169
  const durationMs = Date.now() - startTime;
@@ -0,0 +1,13 @@
1
+ import * as nitropack from 'nitropack';
2
+
3
+ /**
4
+ * Custom Nitro error handler that properly serializes EvlogError.
5
+ * This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
6
+ * in the JSON response regardless of the underlying HTTP framework.
7
+ *
8
+ * For non-EvlogError, it preserves Nitro's default response shape while
9
+ * sanitizing internal error details in production for 5xx errors.
10
+ */
11
+ declare const _default: nitropack.NitroErrorHandler;
12
+
13
+ export { _default as default };
@@ -0,0 +1,13 @@
1
+ import * as nitropack from 'nitropack';
2
+
3
+ /**
4
+ * Custom Nitro error handler that properly serializes EvlogError.
5
+ * This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
6
+ * in the JSON response regardless of the underlying HTTP framework.
7
+ *
8
+ * For non-EvlogError, it preserves Nitro's default response shape while
9
+ * sanitizing internal error details in production for 5xx errors.
10
+ */
11
+ declare const _default: nitropack.NitroErrorHandler;
12
+
13
+ export { _default as default };
@@ -0,0 +1,41 @@
1
+ import { defineNitroErrorHandler } from 'nitropack/runtime';
2
+ import { getRequestURL, setResponseStatus, setResponseHeader, send } from 'h3';
3
+
4
+ const errorHandler = defineNitroErrorHandler((error, event) => {
5
+ const evlogError = error.name === "EvlogError" ? error : error.cause?.name === "EvlogError" ? error.cause : null;
6
+ const isDev = process.env.NODE_ENV === "development";
7
+ const url = getRequestURL(event, { xForwardedHost: true }).pathname;
8
+ if (!evlogError) {
9
+ const status2 = error.statusCode ?? error.status ?? 500;
10
+ const rawMessage = (error.statusText ?? error.statusMessage ?? error.message) || "Internal Server Error";
11
+ const message = isDev ? rawMessage : status2 >= 500 ? "Internal Server Error" : rawMessage;
12
+ setResponseStatus(event, status2);
13
+ setResponseHeader(event, "Content-Type", "application/json");
14
+ return send(event, JSON.stringify({
15
+ url,
16
+ status: status2,
17
+ statusCode: status2,
18
+ statusText: message,
19
+ statusMessage: message,
20
+ message,
21
+ error: true
22
+ }));
23
+ }
24
+ const status = evlogError.status ?? evlogError.statusCode ?? 500;
25
+ setResponseStatus(event, status);
26
+ setResponseHeader(event, "Content-Type", "application/json");
27
+ const { data } = evlogError;
28
+ const statusMessage = evlogError.statusMessage || evlogError.message;
29
+ return send(event, JSON.stringify({
30
+ url,
31
+ status,
32
+ statusCode: status,
33
+ statusText: statusMessage,
34
+ statusMessage,
35
+ message: evlogError.message,
36
+ error: true,
37
+ ...data !== void 0 && { data }
38
+ }));
39
+ });
40
+
41
+ export { errorHandler as default };
@@ -1,13 +1,38 @@
1
1
  import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
2
+ import { getHeaders } from 'h3';
2
3
  import { initLogger, createRequestLogger } from '../logger.mjs';
3
4
  import { matchesPattern } from '../utils.mjs';
5
+ import 'defu';
4
6
 
5
- function shouldLog(path, include) {
7
+ function shouldLog(path, include, exclude) {
8
+ if (exclude && exclude.length > 0) {
9
+ if (exclude.some((pattern) => matchesPattern(path, pattern))) {
10
+ return false;
11
+ }
12
+ }
6
13
  if (!include || include.length === 0) {
7
14
  return true;
8
15
  }
9
16
  return include.some((pattern) => matchesPattern(path, pattern));
10
17
  }
18
+ const SENSITIVE_HEADERS = [
19
+ "authorization",
20
+ "cookie",
21
+ "set-cookie",
22
+ "x-api-key",
23
+ "x-auth-token",
24
+ "proxy-authorization"
25
+ ];
26
+ function getSafeHeaders(event) {
27
+ const allHeaders = getHeaders(event);
28
+ const safeHeaders = {};
29
+ for (const [key, value] of Object.entries(allHeaders)) {
30
+ if (!SENSITIVE_HEADERS.includes(key.toLowerCase())) {
31
+ safeHeaders[key] = value;
32
+ }
33
+ }
34
+ return safeHeaders;
35
+ }
11
36
  function getResponseStatus(event) {
12
37
  if (event.node?.res?.statusCode) {
13
38
  return event.node.res.statusCode;
@@ -24,7 +49,8 @@ function callDrainHook(nitroApp, emittedEvent, event) {
24
49
  if (emittedEvent) {
25
50
  nitroApp.hooks.callHook("evlog:drain", {
26
51
  event: emittedEvent,
27
- request: { method: event.method, path: event.path, requestId: event.context.requestId }
52
+ request: { method: event.method, path: event.path, requestId: event.context.requestId },
53
+ headers: getSafeHeaders(event)
28
54
  }).catch((err) => {
29
55
  console.error("[evlog] drain failed:", err);
30
56
  });
@@ -40,7 +66,7 @@ const plugin = defineNitroPlugin((nitroApp) => {
40
66
  });
41
67
  nitroApp.hooks.hook("request", (event) => {
42
68
  const e = event;
43
- if (!shouldLog(e.path, evlogConfig?.include)) {
69
+ if (!shouldLog(e.path, evlogConfig?.include, evlogConfig?.exclude)) {
44
70
  return;
45
71
  }
46
72
  e.context._evlogStartTime = Date.now();
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { EnvironmentContext, SamplingConfig } from '../types.mjs';
2
+ import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.mjs';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -18,6 +18,13 @@ interface ModuleOptions {
18
18
  * @example ['/api/**', '/auth/**']
19
19
  */
20
20
  include?: string[];
21
+ /**
22
+ * Route patterns to exclude from logging.
23
+ * Supports glob patterns like '/api/_nuxt_icon/**'.
24
+ * Exclusions take precedence over inclusions.
25
+ * @example ['/api/_nuxt_icon/**', '/health']
26
+ */
27
+ exclude?: string[];
21
28
  /**
22
29
  * Sampling configuration for filtering logs.
23
30
  * Allows configuring what percentage of logs to keep per level.
@@ -35,6 +42,18 @@ interface ModuleOptions {
35
42
  * ```
36
43
  */
37
44
  sampling?: SamplingConfig;
45
+ /**
46
+ * Transport configuration for sending client logs to the server.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * transport: {
51
+ * enabled: true, // Send logs to server API
52
+ * endpoint: '/api/_evlog/ingest' // Custom endpoint
53
+ * }
54
+ * ```
55
+ */
56
+ transport?: TransportConfig;
38
57
  }
39
58
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
40
59
 
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { EnvironmentContext, SamplingConfig } from '../types.js';
2
+ import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -18,6 +18,13 @@ interface ModuleOptions {
18
18
  * @example ['/api/**', '/auth/**']
19
19
  */
20
20
  include?: string[];
21
+ /**
22
+ * Route patterns to exclude from logging.
23
+ * Supports glob patterns like '/api/_nuxt_icon/**'.
24
+ * Exclusions take precedence over inclusions.
25
+ * @example ['/api/_nuxt_icon/**', '/health']
26
+ */
27
+ exclude?: string[];
21
28
  /**
22
29
  * Sampling configuration for filtering logs.
23
30
  * Allows configuring what percentage of logs to keep per level.
@@ -35,6 +42,18 @@ interface ModuleOptions {
35
42
  * ```
36
43
  */
37
44
  sampling?: SamplingConfig;
45
+ /**
46
+ * Transport configuration for sending client logs to the server.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * transport: {
51
+ * enabled: true, // Send logs to server API
52
+ * endpoint: '/api/_evlog/ingest' // Custom endpoint
53
+ * }
54
+ * ```
55
+ */
56
+ transport?: TransportConfig;
38
57
  }
39
58
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
40
59
 
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addServerPlugin, addPlugin, addImports, addServerImports } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addServerHandler, addServerPlugin, addPlugin, addImports, addServerImports } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -9,10 +9,26 @@ const module$1 = defineNuxtModule({
9
9
  defaults: {},
10
10
  setup(options, nuxt) {
11
11
  const resolver = createResolver(import.meta.url);
12
+ const transportEnabled = options.transport?.enabled ?? false;
13
+ const transportEndpoint = options.transport?.endpoint ?? "/api/_evlog/ingest";
14
+ nuxt.hook("nitro:config", (nitroConfig) => {
15
+ nitroConfig.errorHandler = nitroConfig.errorHandler || resolver.resolve("../nitro/errorHandler");
16
+ });
12
17
  nuxt.options.runtimeConfig.evlog = options;
13
18
  nuxt.options.runtimeConfig.public.evlog = {
14
- pretty: options.pretty
19
+ pretty: options.pretty,
20
+ transport: {
21
+ enabled: transportEnabled,
22
+ endpoint: transportEndpoint
23
+ }
15
24
  };
25
+ if (transportEnabled) {
26
+ addServerHandler({
27
+ route: transportEndpoint,
28
+ method: "post",
29
+ handler: resolver.resolve("../runtime/server/routes/_evlog/ingest.post")
30
+ });
31
+ }
16
32
  addServerPlugin(resolver.resolve("../nitro/plugin"));
17
33
  addPlugin({
18
34
  src: resolver.resolve("../runtime/client/plugin"),
@@ -1,8 +1,9 @@
1
- import { Log } from '../../types.mjs';
1
+ import { TransportConfig, Log } from '../../types.mjs';
2
2
 
3
3
  declare function initLog(options?: {
4
4
  pretty?: boolean;
5
5
  service?: string;
6
+ transport?: TransportConfig;
6
7
  }): void;
7
8
  declare const log: Log;
8
9
 
@@ -1,8 +1,9 @@
1
- import { Log } from '../../types.js';
1
+ import { TransportConfig, Log } from '../../types.js';
2
2
 
3
3
  declare function initLog(options?: {
4
4
  pretty?: boolean;
5
5
  service?: string;
6
+ transport?: TransportConfig;
6
7
  }): void;
7
8
  declare const log: Log;
8
9
 
@@ -3,6 +3,8 @@ import { getConsoleMethod } from '../../utils.mjs';
3
3
  const isClient = typeof window !== "undefined";
4
4
  let clientPretty = true;
5
5
  let clientService = "client";
6
+ let transportEnabled = false;
7
+ let transportEndpoint = "/api/_evlog/ingest";
6
8
  const LEVEL_COLORS = {
7
9
  error: "color: #ef4444; font-weight: bold",
8
10
  warn: "color: #f59e0b; font-weight: bold",
@@ -12,6 +14,21 @@ const LEVEL_COLORS = {
12
14
  function initLog(options = {}) {
13
15
  clientPretty = options.pretty ?? true;
14
16
  clientService = options.service ?? "client";
17
+ transportEnabled = options.transport?.enabled ?? false;
18
+ transportEndpoint = options.transport?.endpoint ?? "/api/_evlog/ingest";
19
+ }
20
+ async function sendToServer(event) {
21
+ if (!transportEnabled) return;
22
+ try {
23
+ await fetch(transportEndpoint, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify(event),
27
+ keepalive: true,
28
+ credentials: "same-origin"
29
+ });
30
+ } catch {
31
+ }
15
32
  }
16
33
  function emitLog(level, event) {
17
34
  const formatted = {
@@ -27,10 +44,18 @@ function emitLog(level, event) {
27
44
  } else {
28
45
  console[method](JSON.stringify(formatted));
29
46
  }
47
+ sendToServer(formatted);
30
48
  }
31
49
  function emitTaggedLog(level, tag, message) {
32
50
  if (clientPretty) {
33
51
  console[getConsoleMethod(level)](`%c[${tag}]%c ${message}`, LEVEL_COLORS[level] || "", "color: inherit");
52
+ sendToServer({
53
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
54
+ level,
55
+ service: clientService,
56
+ tag,
57
+ message
58
+ });
34
59
  } else {
35
60
  emitLog(level, { tag, message });
36
61
  }
@@ -7,7 +7,8 @@ const plugin = defineNuxtPlugin(() => {
7
7
  const evlogConfig = config.public?.evlog;
8
8
  initLog({
9
9
  pretty: evlogConfig?.pretty ?? import.meta.dev,
10
- service: "client"
10
+ service: "client",
11
+ transport: evlogConfig?.transport
11
12
  });
12
13
  });
13
14
 
@@ -0,0 +1,5 @@
1
+ import * as h3 from 'h3';
2
+
3
+ declare const _default: h3.EventHandler<h3.EventHandlerRequest, Promise<null>>;
4
+
5
+ export { _default as default };
@@ -0,0 +1,5 @@
1
+ import * as h3 from 'h3';
2
+
3
+ declare const _default: h3.EventHandler<h3.EventHandlerRequest, Promise<null>>;
4
+
5
+ export { _default as default };
@@ -0,0 +1,87 @@
1
+ import { defineEventHandler, readBody, setResponseStatus, getHeader, getRequestHost, createError } from 'h3';
2
+ import { useNitroApp } from 'nitropack/runtime';
3
+ import { getEnvironment } from '../../../../logger.mjs';
4
+ import 'defu';
5
+ import '../../../../utils.mjs';
6
+
7
+ const VALID_LEVELS = ["info", "error", "warn", "debug"];
8
+ function validateOrigin(event) {
9
+ const origin = getHeader(event, "origin");
10
+ const referer = getHeader(event, "referer");
11
+ const host = getRequestHost(event);
12
+ const requestOrigin = origin || (referer ? new URL(referer).origin : null);
13
+ if (!requestOrigin) {
14
+ throw createError({ statusCode: 403, message: "Missing origin header" });
15
+ }
16
+ const originHost = new URL(requestOrigin).host;
17
+ if (originHost !== host) {
18
+ throw createError({ statusCode: 403, message: "Invalid origin" });
19
+ }
20
+ }
21
+ const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
22
+ function isValidISOTimestamp(value) {
23
+ if (!ISO_8601_REGEX.test(value)) return false;
24
+ const date = new Date(value);
25
+ return !Number.isNaN(date.getTime());
26
+ }
27
+ function validatePayload(body) {
28
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
29
+ throw createError({ statusCode: 400, message: "Invalid request body" });
30
+ }
31
+ const payload = body;
32
+ if (payload.timestamp === void 0 || payload.timestamp === null) {
33
+ throw createError({ statusCode: 400, message: "Missing required field: timestamp" });
34
+ }
35
+ const { timestamp: rawTimestamp } = payload;
36
+ let timestamp;
37
+ if (typeof rawTimestamp === "number") {
38
+ const minTimestamp = (/* @__PURE__ */ new Date("2000-01-01")).getTime();
39
+ const maxTimestamp = Date.now() + 24 * 60 * 60 * 1e3;
40
+ if (rawTimestamp < minTimestamp || rawTimestamp > maxTimestamp) {
41
+ throw createError({ statusCode: 400, message: "Invalid timestamp: value out of reasonable range" });
42
+ }
43
+ timestamp = new Date(rawTimestamp).toISOString();
44
+ } else if (typeof rawTimestamp === "string") {
45
+ if (!isValidISOTimestamp(rawTimestamp)) {
46
+ throw createError({ statusCode: 400, message: "Invalid timestamp: must be a valid ISO 8601 datetime string" });
47
+ }
48
+ timestamp = rawTimestamp;
49
+ } else {
50
+ throw createError({ statusCode: 400, message: "Invalid timestamp: must be string or number" });
51
+ }
52
+ if (!payload.level || typeof payload.level !== "string") {
53
+ throw createError({ statusCode: 400, message: "Missing required field: level" });
54
+ }
55
+ if (!VALID_LEVELS.includes(payload.level)) {
56
+ throw createError({ statusCode: 400, message: `Invalid level: must be one of ${VALID_LEVELS.join(", ")}` });
57
+ }
58
+ return {
59
+ ...payload,
60
+ timestamp,
61
+ level: payload.level
62
+ };
63
+ }
64
+ const ingest_post = defineEventHandler(async (event) => {
65
+ validateOrigin(event);
66
+ const body = await readBody(event);
67
+ const payload = validatePayload(body);
68
+ const nitroApp = useNitroApp();
69
+ const env = getEnvironment();
70
+ const { service: _clientService, ...sanitizedPayload } = payload;
71
+ const wideEvent = {
72
+ ...sanitizedPayload,
73
+ ...env,
74
+ source: "client"
75
+ };
76
+ try {
77
+ await nitroApp.hooks.callHook("evlog:drain", {
78
+ event: wideEvent,
79
+ request: { method: "POST", path: event.path }
80
+ });
81
+ } catch {
82
+ }
83
+ setResponseStatus(event, 204);
84
+ return null;
85
+ });
86
+
87
+ export { ingest_post as default };
@@ -1,10 +1,12 @@
1
1
  function parseError(error) {
2
2
  if (error && typeof error === "object" && "data" in error) {
3
- const { data, message: fetchMessage, statusCode: fetchStatusCode } = error;
4
- const evlogData = data?.data;
3
+ const { data, message: fetchMessage, statusCode: fetchStatusCode, status: fetchStatus } = error;
4
+ const evlogData = data?.data ?? data;
5
5
  return {
6
- message: data?.message || fetchMessage || "An error occurred",
7
- status: data?.statusCode || fetchStatusCode || 500,
6
+ // Prefer statusText, then statusMessage (or message) for the error message
7
+ message: data?.statusText || data?.statusMessage || data?.message || fetchMessage || "An error occurred",
8
+ // Prefer status, then statusCode for the status value
9
+ status: data?.status || data?.statusCode || fetchStatus || fetchStatusCode || 500,
8
10
  why: evlogData?.why,
9
11
  fix: evlogData?.fix,
10
12
  link: evlogData?.link,
package/dist/types.d.mts CHANGED
@@ -33,6 +33,29 @@ declare module 'nitropack/types' {
33
33
  'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
34
34
  }
35
35
  }
36
+ /**
37
+ * Transport configuration for sending client logs to the server
38
+ */
39
+ interface TransportConfig {
40
+ /**
41
+ * Enable sending logs to the server API
42
+ * @default false
43
+ */
44
+ enabled?: boolean;
45
+ /**
46
+ * API endpoint for log ingestion
47
+ * @default '/api/_evlog/ingest'
48
+ */
49
+ endpoint?: string;
50
+ }
51
+ /**
52
+ * Payload sent from client to server for log ingestion
53
+ */
54
+ interface IngestPayload {
55
+ timestamp: string;
56
+ level: 'info' | 'error' | 'warn' | 'debug';
57
+ [key: string]: unknown;
58
+ }
36
59
  /**
37
60
  * Sampling rates per log level (0-100 percentage)
38
61
  */
@@ -92,6 +115,8 @@ interface DrainContext {
92
115
  path?: string;
93
116
  requestId?: string;
94
117
  };
118
+ /** HTTP headers from the original request (useful for correlation with external services) */
119
+ headers?: Record<string, string>;
95
120
  }
96
121
  /**
97
122
  * Sampling configuration for filtering logs
@@ -190,7 +215,7 @@ type WideEvent = BaseWideEvent & Record<string, unknown>;
190
215
  */
191
216
  interface RequestLogger {
192
217
  /**
193
- * Add context to the wide event (shallow merge)
218
+ * Add context to the wide event (deep merge via defu)
194
219
  */
195
220
  set: <T extends Record<string, unknown>>(context: T) => void;
196
221
  /**
@@ -314,4 +339,4 @@ interface ParsedError {
314
339
  raw: unknown;
315
340
  }
316
341
 
317
- export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
342
+ export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
package/dist/types.d.ts CHANGED
@@ -33,6 +33,29 @@ declare module 'nitropack/types' {
33
33
  'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
34
34
  }
35
35
  }
36
+ /**
37
+ * Transport configuration for sending client logs to the server
38
+ */
39
+ interface TransportConfig {
40
+ /**
41
+ * Enable sending logs to the server API
42
+ * @default false
43
+ */
44
+ enabled?: boolean;
45
+ /**
46
+ * API endpoint for log ingestion
47
+ * @default '/api/_evlog/ingest'
48
+ */
49
+ endpoint?: string;
50
+ }
51
+ /**
52
+ * Payload sent from client to server for log ingestion
53
+ */
54
+ interface IngestPayload {
55
+ timestamp: string;
56
+ level: 'info' | 'error' | 'warn' | 'debug';
57
+ [key: string]: unknown;
58
+ }
36
59
  /**
37
60
  * Sampling rates per log level (0-100 percentage)
38
61
  */
@@ -92,6 +115,8 @@ interface DrainContext {
92
115
  path?: string;
93
116
  requestId?: string;
94
117
  };
118
+ /** HTTP headers from the original request (useful for correlation with external services) */
119
+ headers?: Record<string, string>;
95
120
  }
96
121
  /**
97
122
  * Sampling configuration for filtering logs
@@ -190,7 +215,7 @@ type WideEvent = BaseWideEvent & Record<string, unknown>;
190
215
  */
191
216
  interface RequestLogger {
192
217
  /**
193
- * Add context to the wide event (shallow merge)
218
+ * Add context to the wide event (deep merge via defu)
194
219
  */
195
220
  set: <T extends Record<string, unknown>>(context: T) => void;
196
221
  /**
@@ -314,4 +339,4 @@ interface ParsedError {
314
339
  raw: unknown;
315
340
  }
316
341
 
317
- export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
342
+ export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evlog",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Wide event logging library with structured error handling. Inspired by LoggingSucks.",
5
5
  "author": "HugoRCD <contact@hrcd.fr>",
6
6
  "homepage": "https://evlog.dev",
@@ -67,7 +67,8 @@
67
67
  "typecheck": "echo 'Typecheck handled by build'"
68
68
  },
69
69
  "dependencies": {
70
- "@nuxt/kit": "^4.3.0"
70
+ "@nuxt/kit": "^4.3.0",
71
+ "defu": "^6.1.4"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@nuxt/devtools": "^3.1.1",