evlog 1.7.0 → 1.9.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.
Files changed (81) hide show
  1. package/README.md +257 -61
  2. package/dist/_http-DVDwNag0.mjs +76 -0
  3. package/dist/_http-DVDwNag0.mjs.map +1 -0
  4. package/dist/_severity-CXfyvxQi.mjs +17 -0
  5. package/dist/_severity-CXfyvxQi.mjs.map +1 -0
  6. package/dist/adapters/axiom.d.mts +1 -0
  7. package/dist/adapters/axiom.d.mts.map +1 -1
  8. package/dist/adapters/axiom.mjs +40 -44
  9. package/dist/adapters/axiom.mjs.map +1 -1
  10. package/dist/adapters/better-stack.d.mts +1 -0
  11. package/dist/adapters/better-stack.d.mts.map +1 -1
  12. package/dist/adapters/better-stack.mjs +34 -45
  13. package/dist/adapters/better-stack.mjs.map +1 -1
  14. package/dist/adapters/otlp.d.mts +1 -0
  15. package/dist/adapters/otlp.d.mts.map +1 -1
  16. package/dist/adapters/otlp.mjs +61 -81
  17. package/dist/adapters/otlp.mjs.map +1 -1
  18. package/dist/adapters/posthog.d.mts +35 -1
  19. package/dist/adapters/posthog.d.mts.map +1 -1
  20. package/dist/adapters/posthog.mjs +91 -45
  21. package/dist/adapters/posthog.mjs.map +1 -1
  22. package/dist/adapters/sentry.d.mts +1 -0
  23. package/dist/adapters/sentry.d.mts.map +1 -1
  24. package/dist/adapters/sentry.mjs +41 -53
  25. package/dist/adapters/sentry.mjs.map +1 -1
  26. package/dist/browser.d.mts +63 -0
  27. package/dist/browser.d.mts.map +1 -0
  28. package/dist/browser.mjs +95 -0
  29. package/dist/browser.mjs.map +1 -0
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/logger.d.mts +6 -2
  33. package/dist/logger.d.mts.map +1 -1
  34. package/dist/logger.mjs +56 -3
  35. package/dist/logger.mjs.map +1 -1
  36. package/dist/nitro/errorHandler.mjs +6 -17
  37. package/dist/nitro/errorHandler.mjs.map +1 -1
  38. package/dist/nitro/module.d.mts +11 -0
  39. package/dist/nitro/module.d.mts.map +1 -0
  40. package/dist/nitro/module.mjs +23 -0
  41. package/dist/nitro/module.mjs.map +1 -0
  42. package/dist/nitro/plugin.mjs +28 -52
  43. package/dist/nitro/plugin.mjs.map +1 -1
  44. package/dist/nitro/v3/errorHandler.d.mts +24 -0
  45. package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
  46. package/dist/nitro/v3/errorHandler.mjs +36 -0
  47. package/dist/nitro/v3/errorHandler.mjs.map +1 -0
  48. package/dist/nitro/v3/index.d.mts +4 -0
  49. package/dist/nitro/v3/index.mjs +4 -0
  50. package/dist/nitro/v3/module.d.mts +10 -0
  51. package/dist/nitro/v3/module.d.mts.map +1 -0
  52. package/dist/nitro/v3/module.mjs +22 -0
  53. package/dist/nitro/v3/module.mjs.map +1 -0
  54. package/dist/nitro/v3/plugin.d.mts +14 -0
  55. package/dist/nitro/v3/plugin.d.mts.map +1 -0
  56. package/dist/nitro/v3/plugin.mjs +157 -0
  57. package/dist/nitro/v3/plugin.mjs.map +1 -0
  58. package/dist/nitro/v3/useLogger.d.mts +24 -0
  59. package/dist/nitro/v3/useLogger.d.mts.map +1 -0
  60. package/dist/nitro/v3/useLogger.mjs +27 -0
  61. package/dist/nitro/v3/useLogger.mjs.map +1 -0
  62. package/dist/nitro-D57TWGyN.mjs +73 -0
  63. package/dist/nitro-D57TWGyN.mjs.map +1 -0
  64. package/dist/nitro-D81NBVPi.d.mts +42 -0
  65. package/dist/nitro-D81NBVPi.d.mts.map +1 -0
  66. package/dist/nuxt/module.d.mts +12 -0
  67. package/dist/nuxt/module.d.mts.map +1 -1
  68. package/dist/nuxt/module.mjs +17 -2
  69. package/dist/nuxt/module.mjs.map +1 -1
  70. package/dist/runtime/client/log.d.mts +5 -2
  71. package/dist/runtime/client/log.d.mts.map +1 -1
  72. package/dist/runtime/client/log.mjs +16 -3
  73. package/dist/runtime/client/log.mjs.map +1 -1
  74. package/dist/runtime/client/plugin.mjs +1 -0
  75. package/dist/runtime/client/plugin.mjs.map +1 -1
  76. package/dist/runtime/server/routes/_evlog/ingest.post.mjs +1 -1
  77. package/dist/types.d.mts +32 -2
  78. package/dist/types.d.mts.map +1 -1
  79. package/package.json +30 -7
  80. package/dist/_utils-DZA9nou3.mjs +0 -23
  81. package/dist/_utils-DZA9nou3.mjs.map +0 -1
@@ -1,6 +1,20 @@
1
- import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
1
+ import { n as defineDrain, r as resolveAdapterConfig, t as httpPost } from "../_http-DVDwNag0.mjs";
2
+ import { sendBatchToOTLP } from "./otlp.mjs";
2
3
 
3
4
  //#region src/adapters/posthog.ts
5
+ const POSTHOG_FIELDS = [
6
+ {
7
+ key: "apiKey",
8
+ env: ["NUXT_POSTHOG_API_KEY", "POSTHOG_API_KEY"]
9
+ },
10
+ {
11
+ key: "host",
12
+ env: ["NUXT_POSTHOG_HOST", "POSTHOG_HOST"]
13
+ },
14
+ { key: "eventName" },
15
+ { key: "distinctId" },
16
+ { key: "timeout" }
17
+ ];
4
18
  /**
5
19
  * Convert a WideEvent to a PostHog event format.
6
20
  */
@@ -8,7 +22,7 @@ function toPostHogEvent(event, config) {
8
22
  const { timestamp, level, service, ...rest } = event;
9
23
  return {
10
24
  event: config.eventName ?? "evlog_wide_event",
11
- distinct_id: config.distinctId ?? service,
25
+ distinct_id: config.distinctId ?? (typeof event.userId === "string" ? event.userId : void 0) ?? service,
12
26
  timestamp,
13
27
  properties: {
14
28
  level,
@@ -39,29 +53,18 @@ function toPostHogEvent(event, config) {
39
53
  * ```
40
54
  */
41
55
  function createPostHogDrain(overrides) {
42
- return async (ctx) => {
43
- const contexts = Array.isArray(ctx) ? ctx : [ctx];
44
- if (contexts.length === 0) return;
45
- const runtimeConfig = getRuntimeConfig();
46
- const evlogPostHog = runtimeConfig?.evlog?.posthog;
47
- const rootPostHog = runtimeConfig?.posthog;
48
- const config = {
49
- apiKey: overrides?.apiKey ?? evlogPostHog?.apiKey ?? rootPostHog?.apiKey ?? process.env.NUXT_POSTHOG_API_KEY ?? process.env.POSTHOG_API_KEY,
50
- host: overrides?.host ?? evlogPostHog?.host ?? rootPostHog?.host ?? process.env.NUXT_POSTHOG_HOST ?? process.env.POSTHOG_HOST,
51
- eventName: overrides?.eventName ?? evlogPostHog?.eventName ?? rootPostHog?.eventName,
52
- distinctId: overrides?.distinctId ?? evlogPostHog?.distinctId ?? rootPostHog?.distinctId,
53
- timeout: overrides?.timeout ?? evlogPostHog?.timeout ?? rootPostHog?.timeout
54
- };
55
- if (!config.apiKey) {
56
- console.error("[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()");
57
- return;
58
- }
59
- try {
60
- await sendBatchToPostHog(contexts.map((c) => c.event), config);
61
- } catch (error) {
62
- console.error("[evlog/posthog] Failed to send events to PostHog:", error);
63
- }
64
- };
56
+ return defineDrain({
57
+ name: "posthog",
58
+ resolve: () => {
59
+ const config = resolveAdapterConfig("posthog", POSTHOG_FIELDS, overrides);
60
+ if (!config.apiKey) {
61
+ console.error("[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()");
62
+ return null;
63
+ }
64
+ return config;
65
+ },
66
+ send: sendBatchToPostHog
67
+ });
65
68
  }
66
69
  /**
67
70
  * Send a single event to PostHog.
@@ -88,33 +91,76 @@ async function sendToPostHog(event, config) {
88
91
  */
89
92
  async function sendBatchToPostHog(events, config) {
90
93
  if (events.length === 0) return;
91
- const host = (config.host ?? "https://us.i.posthog.com").replace(/\/$/, "");
92
- const timeout = config.timeout ?? 5e3;
93
- const url = `${host}/batch/`;
94
+ const url = `${(config.host ?? "https://us.i.posthog.com").replace(/\/$/, "")}/batch/`;
94
95
  const batch = events.map((event) => toPostHogEvent(event, config));
95
96
  const payload = {
96
97
  api_key: config.apiKey,
97
98
  batch
98
99
  };
99
- const controller = new AbortController();
100
- const timeoutId = setTimeout(() => controller.abort(), timeout);
101
- try {
102
- const response = await fetch(url, {
103
- method: "POST",
104
- headers: { "Content-Type": "application/json" },
105
- body: JSON.stringify(payload),
106
- signal: controller.signal
107
- });
108
- if (!response.ok) {
109
- const text = await response.text().catch(() => "Unknown error");
110
- const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
111
- throw new Error(`PostHog API error: ${response.status} ${response.statusText} - ${safeText}`);
100
+ await httpPost({
101
+ url,
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify(payload),
104
+ timeout: config.timeout ?? 5e3,
105
+ label: "PostHog"
106
+ });
107
+ }
108
+ const POSTHOG_LOGS_FIELDS = [
109
+ {
110
+ key: "apiKey",
111
+ env: ["NUXT_POSTHOG_API_KEY", "POSTHOG_API_KEY"]
112
+ },
113
+ {
114
+ key: "host",
115
+ env: ["NUXT_POSTHOG_HOST", "POSTHOG_HOST"]
116
+ },
117
+ { key: "timeout" }
118
+ ];
119
+ /**
120
+ * Create a drain function for sending logs to PostHog Logs via OTLP.
121
+ *
122
+ * PostHog Logs uses the standard OTLP log format. This drain wraps
123
+ * `sendBatchToOTLP()` with PostHog-specific defaults (endpoint, auth).
124
+ *
125
+ * Configuration priority (highest to lowest):
126
+ * 1. Overrides passed to createPostHogLogsDrain()
127
+ * 2. runtimeConfig.evlog.posthog
128
+ * 3. runtimeConfig.posthog
129
+ * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * // Zero config - just set NUXT_POSTHOG_API_KEY env var
134
+ * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain())
135
+ *
136
+ * // With overrides
137
+ * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain({
138
+ * apiKey: 'phc_...',
139
+ * host: 'https://eu.i.posthog.com',
140
+ * }))
141
+ * ```
142
+ */
143
+ function createPostHogLogsDrain(overrides) {
144
+ return defineDrain({
145
+ name: "posthog",
146
+ resolve: () => {
147
+ const config = resolveAdapterConfig("posthog", POSTHOG_LOGS_FIELDS, overrides);
148
+ if (!config.apiKey) {
149
+ console.error("[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogLogsDrain()");
150
+ return null;
151
+ }
152
+ return config;
153
+ },
154
+ send: async (events, config) => {
155
+ await sendBatchToOTLP(events, {
156
+ endpoint: `${(config.host ?? "https://us.i.posthog.com").replace(/\/$/, "")}/i`,
157
+ headers: { Authorization: `Bearer ${config.apiKey}` },
158
+ timeout: config.timeout
159
+ });
112
160
  }
113
- } finally {
114
- clearTimeout(timeoutId);
115
- }
161
+ });
116
162
  }
117
163
 
118
164
  //#endregion
119
- export { createPostHogDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
165
+ export { createPostHogDrain, createPostHogLogsDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
120
166
  //# sourceMappingURL=posthog.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"posthog.mjs","names":[],"sources":["../../src/adapters/posthog.ts"],"sourcesContent":["import type { DrainContext, WideEvent } from '../types'\nimport { getRuntimeConfig } from './_utils'\n\nexport interface PostHogConfig {\n /** PostHog project API key */\n apiKey: string\n /** PostHog host URL. Default: https://us.i.posthog.com */\n host?: string\n /** PostHog event name. Default: evlog_wide_event */\n eventName?: string\n /** Override distinct_id (defaults to event.service) */\n distinctId?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** PostHog event structure for the batch API */\nexport interface PostHogEvent {\n event: string\n distinct_id: string\n timestamp: string\n properties: Record<string, unknown>\n}\n\n/**\n * Convert a WideEvent to a PostHog event format.\n */\nexport function toPostHogEvent(event: WideEvent, config: PostHogConfig): PostHogEvent {\n const { timestamp, level, service, ...rest } = event\n\n return {\n event: config.eventName ?? 'evlog_wide_event',\n distinct_id: config.distinctId ?? service,\n timestamp,\n properties: {\n level,\n service,\n ...rest,\n },\n }\n}\n\n/**\n * Create a drain function for sending logs to PostHog.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createPostHogDrain()\n * 2. runtimeConfig.evlog.posthog\n * 3. runtimeConfig.posthog\n * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_POSTHOG_API_KEY env var\n * nitroApp.hooks.hook('evlog:drain', createPostHogDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createPostHogDrain({\n * apiKey: 'phc_...',\n * host: 'https://eu.i.posthog.com',\n * }))\n * ```\n */\nexport function createPostHogDrain(overrides?: Partial<PostHogConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const runtimeConfig = getRuntimeConfig()\n // Support both runtimeConfig.evlog.posthog and runtimeConfig.posthog\n const evlogPostHog = runtimeConfig?.evlog?.posthog\n const rootPostHog = runtimeConfig?.posthog\n\n // Build config with fallbacks: overrides > evlog.posthog > posthog > env vars (NUXT_POSTHOG_* or POSTHOG_*)\n const config: Partial<PostHogConfig> = {\n apiKey: overrides?.apiKey ?? evlogPostHog?.apiKey ?? rootPostHog?.apiKey ?? process.env.NUXT_POSTHOG_API_KEY ?? process.env.POSTHOG_API_KEY,\n host: overrides?.host ?? evlogPostHog?.host ?? rootPostHog?.host ?? process.env.NUXT_POSTHOG_HOST ?? process.env.POSTHOG_HOST,\n eventName: overrides?.eventName ?? evlogPostHog?.eventName ?? rootPostHog?.eventName,\n distinctId: overrides?.distinctId ?? evlogPostHog?.distinctId ?? rootPostHog?.distinctId,\n timeout: overrides?.timeout ?? evlogPostHog?.timeout ?? rootPostHog?.timeout,\n }\n\n if (!config.apiKey) {\n console.error('[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()')\n return\n }\n\n try {\n await sendBatchToPostHog(contexts.map(c => c.event), config as PostHogConfig)\n } catch (error) {\n console.error('[evlog/posthog] Failed to send events to PostHog:', error)\n }\n }\n}\n\n/**\n * Send a single event to PostHog.\n *\n * @example\n * ```ts\n * await sendToPostHog(event, {\n * apiKey: process.env.POSTHOG_API_KEY!,\n * })\n * ```\n */\nexport async function sendToPostHog(event: WideEvent, config: PostHogConfig): Promise<void> {\n await sendBatchToPostHog([event], config)\n}\n\n/**\n * Send a batch of events to PostHog.\n *\n * @example\n * ```ts\n * await sendBatchToPostHog(events, {\n * apiKey: process.env.POSTHOG_API_KEY!,\n * })\n * ```\n */\nexport async function sendBatchToPostHog(events: WideEvent[], config: PostHogConfig): Promise<void> {\n if (events.length === 0) return\n\n const host = (config.host ?? 'https://us.i.posthog.com').replace(/\\/$/, '')\n const timeout = config.timeout ?? 5000\n const url = `${host}/batch/`\n\n const batch = events.map(event => toPostHogEvent(event, config))\n\n const payload = {\n api_key: config.apiKey,\n batch,\n }\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`PostHog API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":";;;;;;AA2BA,SAAgB,eAAe,OAAkB,QAAqC;CACpF,MAAM,EAAE,WAAW,OAAO,SAAS,GAAG,SAAS;AAE/C,QAAO;EACL,OAAO,OAAO,aAAa;EAC3B,aAAa,OAAO,cAAc;EAClC;EACA,YAAY;GACV;GACA;GACA,GAAG;GACJ;EACF;;;;;;;;;;;;;;;;;;;;;;;AAwBH,SAAgB,mBAAmB,WAA2F;AAC5H,QAAO,OAAO,QAAuC;EACnD,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI;AACjD,MAAI,SAAS,WAAW,EAAG;EAE3B,MAAM,gBAAgB,kBAAkB;EAExC,MAAM,eAAe,eAAe,OAAO;EAC3C,MAAM,cAAc,eAAe;EAGnC,MAAM,SAAiC;GACrC,QAAQ,WAAW,UAAU,cAAc,UAAU,aAAa,UAAU,QAAQ,IAAI,wBAAwB,QAAQ,IAAI;GAC5H,MAAM,WAAW,QAAQ,cAAc,QAAQ,aAAa,QAAQ,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;GACjH,WAAW,WAAW,aAAa,cAAc,aAAa,aAAa;GAC3E,YAAY,WAAW,cAAc,cAAc,cAAc,aAAa;GAC9E,SAAS,WAAW,WAAW,cAAc,WAAW,aAAa;GACtE;AAED,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAQ,MAAM,mHAAmH;AACjI;;AAGF,MAAI;AACF,SAAM,mBAAmB,SAAS,KAAI,MAAK,EAAE,MAAM,EAAE,OAAwB;WACtE,OAAO;AACd,WAAQ,MAAM,qDAAqD,MAAM;;;;;;;;;;;;;;AAe/E,eAAsB,cAAc,OAAkB,QAAsC;AAC1F,OAAM,mBAAmB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;AAa3C,eAAsB,mBAAmB,QAAqB,QAAsC;AAClG,KAAI,OAAO,WAAW,EAAG;CAEzB,MAAM,QAAQ,OAAO,QAAQ,4BAA4B,QAAQ,OAAO,GAAG;CAC3E,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,MAAM,GAAG,KAAK;CAEpB,MAAM,QAAQ,OAAO,KAAI,UAAS,eAAe,OAAO,OAAO,CAAC;CAEhE,MAAM,UAAU;EACd,SAAS,OAAO;EAChB;EACD;CAED,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR,SAAS,EACP,gBAAgB,oBACjB;GACD,MAAM,KAAK,UAAU,QAAQ;GAC7B,QAAQ,WAAW;GACpB,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,gBAAgB;GAC/D,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,kBAAkB;AAC7E,SAAM,IAAI,MAAM,sBAAsB,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW;;WAEvF;AACR,eAAa,UAAU"}
1
+ {"version":3,"file":"posthog.mjs","names":[],"sources":["../../src/adapters/posthog.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\nimport { sendBatchToOTLP } from './otlp'\nimport type { OTLPConfig } from './otlp'\n\nexport interface PostHogConfig {\n /** PostHog project API key */\n apiKey: string\n /** PostHog host URL. Default: https://us.i.posthog.com */\n host?: string\n /** PostHog event name. Default: evlog_wide_event */\n eventName?: string\n /** Override distinct_id (defaults to event.service) */\n distinctId?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** PostHog event structure for the batch API */\nexport interface PostHogEvent {\n event: string\n distinct_id: string\n timestamp: string\n properties: Record<string, unknown>\n}\n\nconst POSTHOG_FIELDS: ConfigField<PostHogConfig>[] = [\n { key: 'apiKey', env: ['NUXT_POSTHOG_API_KEY', 'POSTHOG_API_KEY'] },\n { key: 'host', env: ['NUXT_POSTHOG_HOST', 'POSTHOG_HOST'] },\n { key: 'eventName' },\n { key: 'distinctId' },\n { key: 'timeout' },\n]\n\n/**\n * Convert a WideEvent to a PostHog event format.\n */\nexport function toPostHogEvent(event: WideEvent, config: PostHogConfig): PostHogEvent {\n const { timestamp, level, service, ...rest } = event\n\n return {\n event: config.eventName ?? 'evlog_wide_event',\n distinct_id: config.distinctId ?? (typeof event.userId === 'string' ? event.userId : undefined) ?? service,\n timestamp,\n properties: {\n level,\n service,\n ...rest,\n },\n }\n}\n\n/**\n * Create a drain function for sending logs to PostHog.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createPostHogDrain()\n * 2. runtimeConfig.evlog.posthog\n * 3. runtimeConfig.posthog\n * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_POSTHOG_API_KEY env var\n * nitroApp.hooks.hook('evlog:drain', createPostHogDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createPostHogDrain({\n * apiKey: 'phc_...',\n * host: 'https://eu.i.posthog.com',\n * }))\n * ```\n */\nexport function createPostHogDrain(overrides?: Partial<PostHogConfig>) {\n return defineDrain<PostHogConfig>({\n name: 'posthog',\n resolve: () => {\n const config = resolveAdapterConfig<PostHogConfig>('posthog', POSTHOG_FIELDS, overrides)\n if (!config.apiKey) {\n console.error('[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()')\n return null\n }\n return config as PostHogConfig\n },\n send: sendBatchToPostHog,\n })\n}\n\n/**\n * Send a single event to PostHog.\n *\n * @example\n * ```ts\n * await sendToPostHog(event, {\n * apiKey: process.env.POSTHOG_API_KEY!,\n * })\n * ```\n */\nexport async function sendToPostHog(event: WideEvent, config: PostHogConfig): Promise<void> {\n await sendBatchToPostHog([event], config)\n}\n\n/**\n * Send a batch of events to PostHog.\n *\n * @example\n * ```ts\n * await sendBatchToPostHog(events, {\n * apiKey: process.env.POSTHOG_API_KEY!,\n * })\n * ```\n */\nexport async function sendBatchToPostHog(events: WideEvent[], config: PostHogConfig): Promise<void> {\n if (events.length === 0) return\n\n const host = (config.host ?? 'https://us.i.posthog.com').replace(/\\/$/, '')\n const url = `${host}/batch/`\n\n const batch = events.map(event => toPostHogEvent(event, config))\n\n const payload = {\n api_key: config.apiKey,\n batch,\n }\n\n await httpPost({\n url,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n timeout: config.timeout ?? 5000,\n label: 'PostHog',\n })\n}\n\nexport interface PostHogLogsConfig {\n /** PostHog project API key */\n apiKey: string\n /** PostHog host URL. Default: https://us.i.posthog.com */\n host?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\nconst POSTHOG_LOGS_FIELDS: ConfigField<PostHogLogsConfig>[] = [\n { key: 'apiKey', env: ['NUXT_POSTHOG_API_KEY', 'POSTHOG_API_KEY'] },\n { key: 'host', env: ['NUXT_POSTHOG_HOST', 'POSTHOG_HOST'] },\n { key: 'timeout' },\n]\n\n/**\n * Create a drain function for sending logs to PostHog Logs via OTLP.\n *\n * PostHog Logs uses the standard OTLP log format. This drain wraps\n * `sendBatchToOTLP()` with PostHog-specific defaults (endpoint, auth).\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createPostHogLogsDrain()\n * 2. runtimeConfig.evlog.posthog\n * 3. runtimeConfig.posthog\n * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_POSTHOG_API_KEY env var\n * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain({\n * apiKey: 'phc_...',\n * host: 'https://eu.i.posthog.com',\n * }))\n * ```\n */\nexport function createPostHogLogsDrain(overrides?: Partial<PostHogLogsConfig>) {\n return defineDrain<PostHogLogsConfig>({\n name: 'posthog',\n resolve: () => {\n const config = resolveAdapterConfig<PostHogLogsConfig>('posthog', POSTHOG_LOGS_FIELDS, overrides)\n if (!config.apiKey) {\n console.error('[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogLogsDrain()')\n return null\n }\n return config as PostHogLogsConfig\n },\n send: async (events, config) => {\n const host = (config.host ?? 'https://us.i.posthog.com').replace(/\\/$/, '')\n const otlpConfig: OTLPConfig = {\n endpoint: `${host}/i`,\n headers: { Authorization: `Bearer ${config.apiKey}` },\n timeout: config.timeout,\n }\n await sendBatchToOTLP(events, otlpConfig)\n },\n })\n}\n"],"mappings":";;;;AA6BA,MAAM,iBAA+C;CACnD;EAAE,KAAK;EAAU,KAAK,CAAC,wBAAwB,kBAAkB;EAAE;CACnE;EAAE,KAAK;EAAQ,KAAK,CAAC,qBAAqB,eAAe;EAAE;CAC3D,EAAE,KAAK,aAAa;CACpB,EAAE,KAAK,cAAc;CACrB,EAAE,KAAK,WAAW;CACnB;;;;AAKD,SAAgB,eAAe,OAAkB,QAAqC;CACpF,MAAM,EAAE,WAAW,OAAO,SAAS,GAAG,SAAS;AAE/C,QAAO;EACL,OAAO,OAAO,aAAa;EAC3B,aAAa,OAAO,eAAe,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS,WAAc;EACnG;EACA,YAAY;GACV;GACA;GACA,GAAG;GACJ;EACF;;;;;;;;;;;;;;;;;;;;;;;AAwBH,SAAgB,mBAAmB,WAAoC;AACrE,QAAO,YAA2B;EAChC,MAAM;EACN,eAAe;GACb,MAAM,SAAS,qBAAoC,WAAW,gBAAgB,UAAU;AACxF,OAAI,CAAC,OAAO,QAAQ;AAClB,YAAQ,MAAM,mHAAmH;AACjI,WAAO;;AAET,UAAO;;EAET,MAAM;EACP,CAAC;;;;;;;;;;;;AAaJ,eAAsB,cAAc,OAAkB,QAAsC;AAC1F,OAAM,mBAAmB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;AAa3C,eAAsB,mBAAmB,QAAqB,QAAsC;AAClG,KAAI,OAAO,WAAW,EAAG;CAGzB,MAAM,MAAM,IADE,OAAO,QAAQ,4BAA4B,QAAQ,OAAO,GAAG,CACvD;CAEpB,MAAM,QAAQ,OAAO,KAAI,UAAS,eAAe,OAAO,OAAO,CAAC;CAEhE,MAAM,UAAU;EACd,SAAS,OAAO;EAChB;EACD;AAED,OAAM,SAAS;EACb;EACA,SAAS,EAAE,gBAAgB,oBAAoB;EAC/C,MAAM,KAAK,UAAU,QAAQ;EAC7B,SAAS,OAAO,WAAW;EAC3B,OAAO;EACR,CAAC;;AAYJ,MAAM,sBAAwD;CAC5D;EAAE,KAAK;EAAU,KAAK,CAAC,wBAAwB,kBAAkB;EAAE;CACnE;EAAE,KAAK;EAAQ,KAAK,CAAC,qBAAqB,eAAe;EAAE;CAC3D,EAAE,KAAK,WAAW;CACnB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,uBAAuB,WAAwC;AAC7E,QAAO,YAA+B;EACpC,MAAM;EACN,eAAe;GACb,MAAM,SAAS,qBAAwC,WAAW,qBAAqB,UAAU;AACjG,OAAI,CAAC,OAAO,QAAQ;AAClB,YAAQ,MAAM,uHAAuH;AACrI,WAAO;;AAET,UAAO;;EAET,MAAM,OAAO,QAAQ,WAAW;AAO9B,SAAM,gBAAgB,QALS;IAC7B,UAAU,IAFE,OAAO,QAAQ,4BAA4B,QAAQ,OAAO,GAAG,CAEvD;IAClB,SAAS,EAAE,eAAe,UAAU,OAAO,UAAU;IACrD,SAAS,OAAO;IACjB,CACwC;;EAE5C,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { DrainContext, WideEvent } from "../types.mjs";
2
+ import "../index.mjs";
2
3
 
3
4
  //#region src/adapters/sentry.d.ts
4
5
  interface SentryConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"sentry.d.mts","names":[],"sources":["../../src/adapters/sentry.ts"],"mappings":";;;UAGiB,YAAA;;EAEf,GAAA;EAF2B;EAI3B,WAAA;EAIa;EAFb,OAAA;EAFA;EAIA,IAAA,GAAO,MAAA;EAAP;EAEA,OAAA;AAAA;;UAIe,oBAAA;EACf,KAAA;EACA,IAAA;AAAA;;UAIe,SAAA;EACf,SAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,eAAA;EACA,UAAA,GAAa,MAAA,SAAe,oBAAA;AAAA;AAAA,iBA0Fd,WAAA,CAAY,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,YAAA,GAAe,SAAA;;;;;;;AAArE;;;;;;;;;;;;;;;AAiGA;;iBAAgB,iBAAA,CAAkB,SAAA,GAAY,OAAA,CAAQ,YAAA,KAAiB,GAAA,EAAK,YAAA,GAAe,YAAA,OAAmB,OAAA;;;;;;;;;;;iBAwCxF,YAAA,CAAa,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,YAAA,GAAe,OAAA;;;;;;AAA5E;;;;;iBAcsB,iBAAA,CAAkB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,YAAA,GAAe,OAAA"}
1
+ {"version":3,"file":"sentry.d.mts","names":[],"sources":["../../src/adapters/sentry.ts"],"mappings":";;;;UAOiB,YAAA;;EAEf,GAAA;EAFe;EAIf,WAAA;;EAEA,OAAA;EAJA;EAMA,IAAA,GAAO,MAAA;EAFP;EAIA,OAAA;AAAA;;UAIe,oBAAA;EACf,KAAA;EACA,IAAA;AAAA;;UAIe,SAAA;EACf,SAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,eAAA;EACA,UAAA,GAAa,MAAA,SAAe,oBAAA;AAAA;AAAA,iBA0Fd,WAAA,CAAY,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,YAAA,GAAe,SAAA;;;;;;;;AAArE;;;;;;;;;;;;;;;AAiGA;iBAAgB,iBAAA,CAAkB,SAAA,GAAY,OAAA,CAAQ,YAAA,KAAa,GAAA,EAAd,YAAA,GAAc,YAAA,OAAA,OAAA;;;;;;;;;;;iBAyB7C,YAAA,CAAa,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,YAAA,GAAe,OAAA;;;;;;;AAA5E;;;;iBAcsB,iBAAA,CAAkB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,YAAA,GAAe,OAAA"}
@@ -1,13 +1,23 @@
1
- import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
1
+ import { n as defineDrain, r as resolveAdapterConfig, t as httpPost } from "../_http-DVDwNag0.mjs";
2
+ import { t as OTEL_SEVERITY_NUMBER } from "../_severity-CXfyvxQi.mjs";
2
3
 
3
4
  //#region src/adapters/sentry.ts
4
- /** Based on OpenTelemetry Logs Data Model specification */
5
- const SEVERITY_MAP = {
6
- debug: 5,
7
- info: 9,
8
- warn: 13,
9
- error: 17
10
- };
5
+ const SENTRY_FIELDS = [
6
+ {
7
+ key: "dsn",
8
+ env: ["NUXT_SENTRY_DSN", "SENTRY_DSN"]
9
+ },
10
+ {
11
+ key: "environment",
12
+ env: ["NUXT_SENTRY_ENVIRONMENT", "SENTRY_ENVIRONMENT"]
13
+ },
14
+ {
15
+ key: "release",
16
+ env: ["NUXT_SENTRY_RELEASE", "SENTRY_RELEASE"]
17
+ },
18
+ { key: "tags" },
19
+ { key: "timeout" }
20
+ ];
11
21
  function parseSentryDsn(dsn) {
12
22
  const url = new URL(dsn);
13
23
  const publicKey = url.username;
@@ -108,7 +118,7 @@ function toSentryLog(event, config) {
108
118
  trace_id: traceId,
109
119
  level,
110
120
  body,
111
- severity_number: SEVERITY_MAP[level] ?? 9,
121
+ severity_number: OTEL_SEVERITY_NUMBER[level] ?? 9,
112
122
  attributes
113
123
  };
114
124
  }
@@ -154,29 +164,18 @@ function buildEnvelopeBody(logs, dsn) {
154
164
  * ```
155
165
  */
156
166
  function createSentryDrain(overrides) {
157
- return async (ctx) => {
158
- const contexts = Array.isArray(ctx) ? ctx : [ctx];
159
- if (contexts.length === 0) return;
160
- const runtimeConfig = getRuntimeConfig();
161
- const evlogSentry = runtimeConfig?.evlog?.sentry;
162
- const rootSentry = runtimeConfig?.sentry;
163
- const config = {
164
- dsn: overrides?.dsn ?? evlogSentry?.dsn ?? rootSentry?.dsn ?? process.env.NUXT_SENTRY_DSN ?? process.env.SENTRY_DSN,
165
- environment: overrides?.environment ?? evlogSentry?.environment ?? rootSentry?.environment ?? process.env.NUXT_SENTRY_ENVIRONMENT ?? process.env.SENTRY_ENVIRONMENT,
166
- release: overrides?.release ?? evlogSentry?.release ?? rootSentry?.release ?? process.env.NUXT_SENTRY_RELEASE ?? process.env.SENTRY_RELEASE,
167
- tags: overrides?.tags ?? evlogSentry?.tags ?? rootSentry?.tags,
168
- timeout: overrides?.timeout ?? evlogSentry?.timeout ?? rootSentry?.timeout
169
- };
170
- if (!config.dsn) {
171
- console.error("[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()");
172
- return;
173
- }
174
- try {
175
- await sendBatchToSentry(contexts.map((c) => c.event), config);
176
- } catch (error) {
177
- console.error("[evlog/sentry] Failed to send events to Sentry:", error);
178
- }
179
- };
167
+ return defineDrain({
168
+ name: "sentry",
169
+ resolve: () => {
170
+ const config = resolveAdapterConfig("sentry", SENTRY_FIELDS, overrides);
171
+ if (!config.dsn) {
172
+ console.error("[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()");
173
+ return null;
174
+ }
175
+ return config;
176
+ },
177
+ send: sendBatchToSentry
178
+ });
180
179
  }
181
180
  /**
182
181
  * Send a single event to Sentry as a structured log.
@@ -204,28 +203,17 @@ async function sendToSentry(event, config) {
204
203
  async function sendBatchToSentry(events, config) {
205
204
  if (events.length === 0) return;
206
205
  const { url, authHeader } = getSentryEnvelopeUrl(config.dsn);
207
- const timeout = config.timeout ?? 5e3;
208
206
  const body = buildEnvelopeBody(events.map((event) => toSentryLog(event, config)), config.dsn);
209
- const controller = new AbortController();
210
- const timeoutId = setTimeout(() => controller.abort(), timeout);
211
- try {
212
- const response = await fetch(url, {
213
- method: "POST",
214
- headers: {
215
- "Content-Type": "application/x-sentry-envelope",
216
- "X-Sentry-Auth": authHeader
217
- },
218
- body,
219
- signal: controller.signal
220
- });
221
- if (!response.ok) {
222
- const text = await response.text().catch(() => "Unknown error");
223
- const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
224
- throw new Error(`Sentry API error: ${response.status} ${response.statusText} - ${safeText}`);
225
- }
226
- } finally {
227
- clearTimeout(timeoutId);
228
- }
207
+ await httpPost({
208
+ url,
209
+ headers: {
210
+ "Content-Type": "application/x-sentry-envelope",
211
+ "X-Sentry-Auth": authHeader
212
+ },
213
+ body,
214
+ timeout: config.timeout ?? 5e3,
215
+ label: "Sentry"
216
+ });
229
217
  }
230
218
 
231
219
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"sentry.mjs","names":[],"sources":["../../src/adapters/sentry.ts"],"sourcesContent":["import type { DrainContext, LogLevel, WideEvent } from '../types'\nimport { getRuntimeConfig } from './_utils'\n\nexport interface SentryConfig {\n /** Sentry DSN */\n dsn: string\n /** Environment override (defaults to event.environment) */\n environment?: string\n /** Release version override (defaults to event.version) */\n release?: string\n /** Additional tags to attach as attributes */\n tags?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** Sentry Log attribute value with type annotation */\nexport interface SentryAttributeValue {\n value: string | number | boolean\n type: 'string' | 'integer' | 'double' | 'boolean'\n}\n\n/** Sentry Structured Log payload */\nexport interface SentryLog {\n timestamp: number\n trace_id: string\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'\n body: string\n severity_number: number\n attributes?: Record<string, SentryAttributeValue>\n}\n\ninterface SentryDsnParts {\n publicKey: string\n secretKey?: string\n projectId: string\n origin: string\n basePath: string\n}\n\n/** Based on OpenTelemetry Logs Data Model specification */\nconst SEVERITY_MAP: Record<LogLevel, number> = {\n debug: 5,\n info: 9,\n warn: 13,\n error: 17,\n}\n\nfunction parseSentryDsn(dsn: string): SentryDsnParts {\n const url = new URL(dsn)\n const publicKey = url.username\n if (!publicKey) {\n throw new Error('Invalid Sentry DSN: missing public key')\n }\n\n const secretKey = url.password || undefined\n\n const pathParts = url.pathname.split('/').filter(Boolean)\n const projectId = pathParts.pop()\n if (!projectId) {\n throw new Error('Invalid Sentry DSN: missing project ID')\n }\n\n const basePath = pathParts.length > 0 ? `/${pathParts.join('/')}` : ''\n\n return {\n publicKey,\n secretKey,\n projectId,\n origin: `${url.protocol}//${url.host}`,\n basePath,\n }\n}\n\nfunction getSentryEnvelopeUrl(dsn: string): { url: string, authHeader: string } {\n const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn)\n const url = `${origin}${basePath}/api/${projectId}/envelope/`\n let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=evlog`\n if (secretKey) {\n authHeader += `, sentry_secret=${secretKey}`\n }\n return { url, authHeader }\n}\n\nfunction createTraceId(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID().replace(/-/g, '')\n }\n\n return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('')\n}\n\nfunction getFirstStringValue(event: WideEvent, keys: string[]): string | undefined {\n for (const key of keys) {\n const value = event[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return undefined\n}\n\nfunction toAttributeValue(value: unknown): SentryAttributeValue | undefined {\n if (value === null || value === undefined) {\n return undefined\n }\n if (typeof value === 'string') {\n return { value, type: 'string' }\n }\n if (typeof value === 'boolean') {\n return { value, type: 'boolean' }\n }\n if (typeof value === 'number') {\n if (Number.isInteger(value)) {\n return { value, type: 'integer' }\n }\n return { value, type: 'double' }\n }\n return { value: JSON.stringify(value), type: 'string' }\n}\n\nexport function toSentryLog(event: WideEvent, config: SentryConfig): SentryLog {\n const { timestamp, level, service, environment, version, ...rest } = event\n\n const body = getFirstStringValue(event, ['message', 'action', 'path'])\n ?? 'evlog wide event'\n\n const traceId = (typeof event.traceId === 'string' && event.traceId.length > 0)\n ? event.traceId\n : createTraceId()\n\n const attributes: Record<string, SentryAttributeValue> = {}\n\n const env = config.environment ?? environment\n if (env) {\n attributes['sentry.environment'] = { value: env, type: 'string' }\n }\n\n const rel = config.release ?? version\n if (typeof rel === 'string' && rel.length > 0) {\n attributes['sentry.release'] = { value: rel, type: 'string' }\n }\n\n attributes['service'] = { value: service, type: 'string' }\n\n if (config.tags) {\n for (const [key, value] of Object.entries(config.tags)) {\n attributes[key] = { value, type: 'string' }\n }\n }\n\n for (const [key, value] of Object.entries(rest)) {\n if (key === 'traceId' || key === 'spanId') continue\n if (value === undefined || value === null) continue\n const attr = toAttributeValue(value)\n if (attr) {\n attributes[key] = attr\n }\n }\n\n return {\n timestamp: new Date(timestamp).getTime() / 1000,\n trace_id: traceId,\n level: level as SentryLog['level'],\n body,\n severity_number: SEVERITY_MAP[level] ?? 9,\n attributes,\n }\n}\n\n/**\n * Build the Sentry Envelope body for a list of logs.\n *\n * Envelope format (line-delimited):\n * - Line 1: Envelope headers (dsn, sent_at)\n * - Line 2: Item header (type: log, item_count, content_type)\n * - Line 3: Item payload ({\"items\": [...]})\n */\nfunction buildEnvelopeBody(logs: SentryLog[], dsn: string): string {\n const envelopeHeader = JSON.stringify({\n dsn,\n sent_at: new Date().toISOString(),\n })\n\n const itemHeader = JSON.stringify({\n type: 'log',\n item_count: logs.length,\n content_type: 'application/vnd.sentry.items.log+json',\n })\n\n const itemPayload = JSON.stringify({ items: logs })\n\n return `${envelopeHeader}\\n${itemHeader}\\n${itemPayload}\\n`\n}\n\n/**\n * Create a drain function for sending logs to Sentry.\n *\n * Sends wide events as Sentry Structured Logs, visible in Explore > Logs\n * in the Sentry dashboard.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createSentryDrain()\n * 2. runtimeConfig.evlog.sentry\n * 3. runtimeConfig.sentry\n * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_SENTRY_DSN env var\n * nitroApp.hooks.hook('evlog:drain', createSentryDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createSentryDrain({\n * dsn: 'https://public@o0.ingest.sentry.io/123',\n * }))\n * ```\n */\nexport function createSentryDrain(overrides?: Partial<SentryConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const runtimeConfig = getRuntimeConfig()\n const evlogSentry = runtimeConfig?.evlog?.sentry\n const rootSentry = runtimeConfig?.sentry\n\n const config: Partial<SentryConfig> = {\n dsn: overrides?.dsn ?? evlogSentry?.dsn ?? rootSentry?.dsn ?? process.env.NUXT_SENTRY_DSN ?? process.env.SENTRY_DSN,\n environment: overrides?.environment ?? evlogSentry?.environment ?? rootSentry?.environment ?? process.env.NUXT_SENTRY_ENVIRONMENT ?? process.env.SENTRY_ENVIRONMENT,\n release: overrides?.release ?? evlogSentry?.release ?? rootSentry?.release ?? process.env.NUXT_SENTRY_RELEASE ?? process.env.SENTRY_RELEASE,\n tags: overrides?.tags ?? evlogSentry?.tags ?? rootSentry?.tags,\n timeout: overrides?.timeout ?? evlogSentry?.timeout ?? rootSentry?.timeout,\n }\n\n if (!config.dsn) {\n console.error('[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()')\n return\n }\n\n try {\n await sendBatchToSentry(contexts.map(c => c.event), config as SentryConfig)\n } catch (error) {\n console.error('[evlog/sentry] Failed to send events to Sentry:', error)\n }\n }\n}\n\n/**\n * Send a single event to Sentry as a structured log.\n *\n * @example\n * ```ts\n * await sendToSentry(event, {\n * dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendToSentry(event: WideEvent, config: SentryConfig): Promise<void> {\n await sendBatchToSentry([event], config)\n}\n\n/**\n * Send a batch of events to Sentry as structured logs via the Envelope endpoint.\n *\n * @example\n * ```ts\n * await sendBatchToSentry(events, {\n * dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendBatchToSentry(events: WideEvent[], config: SentryConfig): Promise<void> {\n if (events.length === 0) return\n\n const { url, authHeader } = getSentryEnvelopeUrl(config.dsn)\n const timeout = config.timeout ?? 5000\n\n const logs = events.map(event => toSentryLog(event, config))\n const body = buildEnvelopeBody(logs, config.dsn)\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-sentry-envelope',\n 'X-Sentry-Auth': authHeader,\n },\n body,\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`Sentry API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sentry.mjs","names":[],"sources":["../../src/adapters/sentry.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\nimport { OTEL_SEVERITY_NUMBER } from './_severity'\n\nexport interface SentryConfig {\n /** Sentry DSN */\n dsn: string\n /** Environment override (defaults to event.environment) */\n environment?: string\n /** Release version override (defaults to event.version) */\n release?: string\n /** Additional tags to attach as attributes */\n tags?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** Sentry Log attribute value with type annotation */\nexport interface SentryAttributeValue {\n value: string | number | boolean\n type: 'string' | 'integer' | 'double' | 'boolean'\n}\n\n/** Sentry Structured Log payload */\nexport interface SentryLog {\n timestamp: number\n trace_id: string\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'\n body: string\n severity_number: number\n attributes?: Record<string, SentryAttributeValue>\n}\n\ninterface SentryDsnParts {\n publicKey: string\n secretKey?: string\n projectId: string\n origin: string\n basePath: string\n}\n\nconst SENTRY_FIELDS: ConfigField<SentryConfig>[] = [\n { key: 'dsn', env: ['NUXT_SENTRY_DSN', 'SENTRY_DSN'] },\n { key: 'environment', env: ['NUXT_SENTRY_ENVIRONMENT', 'SENTRY_ENVIRONMENT'] },\n { key: 'release', env: ['NUXT_SENTRY_RELEASE', 'SENTRY_RELEASE'] },\n { key: 'tags' },\n { key: 'timeout' },\n]\n\nfunction parseSentryDsn(dsn: string): SentryDsnParts {\n const url = new URL(dsn)\n const publicKey = url.username\n if (!publicKey) {\n throw new Error('Invalid Sentry DSN: missing public key')\n }\n\n const secretKey = url.password || undefined\n\n const pathParts = url.pathname.split('/').filter(Boolean)\n const projectId = pathParts.pop()\n if (!projectId) {\n throw new Error('Invalid Sentry DSN: missing project ID')\n }\n\n const basePath = pathParts.length > 0 ? `/${pathParts.join('/')}` : ''\n\n return {\n publicKey,\n secretKey,\n projectId,\n origin: `${url.protocol}//${url.host}`,\n basePath,\n }\n}\n\nfunction getSentryEnvelopeUrl(dsn: string): { url: string, authHeader: string } {\n const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn)\n const url = `${origin}${basePath}/api/${projectId}/envelope/`\n let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=evlog`\n if (secretKey) {\n authHeader += `, sentry_secret=${secretKey}`\n }\n return { url, authHeader }\n}\n\nfunction createTraceId(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID().replace(/-/g, '')\n }\n\n return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('')\n}\n\nfunction getFirstStringValue(event: WideEvent, keys: string[]): string | undefined {\n for (const key of keys) {\n const value = event[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return undefined\n}\n\nfunction toAttributeValue(value: unknown): SentryAttributeValue | undefined {\n if (value === null || value === undefined) {\n return undefined\n }\n if (typeof value === 'string') {\n return { value, type: 'string' }\n }\n if (typeof value === 'boolean') {\n return { value, type: 'boolean' }\n }\n if (typeof value === 'number') {\n if (Number.isInteger(value)) {\n return { value, type: 'integer' }\n }\n return { value, type: 'double' }\n }\n return { value: JSON.stringify(value), type: 'string' }\n}\n\nexport function toSentryLog(event: WideEvent, config: SentryConfig): SentryLog {\n const { timestamp, level, service, environment, version, ...rest } = event\n\n const body = getFirstStringValue(event, ['message', 'action', 'path'])\n ?? 'evlog wide event'\n\n const traceId = (typeof event.traceId === 'string' && event.traceId.length > 0)\n ? event.traceId\n : createTraceId()\n\n const attributes: Record<string, SentryAttributeValue> = {}\n\n const env = config.environment ?? environment\n if (env) {\n attributes['sentry.environment'] = { value: env, type: 'string' }\n }\n\n const rel = config.release ?? version\n if (typeof rel === 'string' && rel.length > 0) {\n attributes['sentry.release'] = { value: rel, type: 'string' }\n }\n\n attributes['service'] = { value: service, type: 'string' }\n\n if (config.tags) {\n for (const [key, value] of Object.entries(config.tags)) {\n attributes[key] = { value, type: 'string' }\n }\n }\n\n for (const [key, value] of Object.entries(rest)) {\n if (key === 'traceId' || key === 'spanId') continue\n if (value === undefined || value === null) continue\n const attr = toAttributeValue(value)\n if (attr) {\n attributes[key] = attr\n }\n }\n\n return {\n timestamp: new Date(timestamp).getTime() / 1000,\n trace_id: traceId,\n level: level as SentryLog['level'],\n body,\n severity_number: OTEL_SEVERITY_NUMBER[level] ?? 9,\n attributes,\n }\n}\n\n/**\n * Build the Sentry Envelope body for a list of logs.\n *\n * Envelope format (line-delimited):\n * - Line 1: Envelope headers (dsn, sent_at)\n * - Line 2: Item header (type: log, item_count, content_type)\n * - Line 3: Item payload ({\"items\": [...]})\n */\nfunction buildEnvelopeBody(logs: SentryLog[], dsn: string): string {\n const envelopeHeader = JSON.stringify({\n dsn,\n sent_at: new Date().toISOString(),\n })\n\n const itemHeader = JSON.stringify({\n type: 'log',\n item_count: logs.length,\n content_type: 'application/vnd.sentry.items.log+json',\n })\n\n const itemPayload = JSON.stringify({ items: logs })\n\n return `${envelopeHeader}\\n${itemHeader}\\n${itemPayload}\\n`\n}\n\n/**\n * Create a drain function for sending logs to Sentry.\n *\n * Sends wide events as Sentry Structured Logs, visible in Explore > Logs\n * in the Sentry dashboard.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createSentryDrain()\n * 2. runtimeConfig.evlog.sentry\n * 3. runtimeConfig.sentry\n * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_SENTRY_DSN env var\n * nitroApp.hooks.hook('evlog:drain', createSentryDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createSentryDrain({\n * dsn: 'https://public@o0.ingest.sentry.io/123',\n * }))\n * ```\n */\nexport function createSentryDrain(overrides?: Partial<SentryConfig>) {\n return defineDrain<SentryConfig>({\n name: 'sentry',\n resolve: () => {\n const config = resolveAdapterConfig<SentryConfig>('sentry', SENTRY_FIELDS, overrides)\n if (!config.dsn) {\n console.error('[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()')\n return null\n }\n return config as SentryConfig\n },\n send: sendBatchToSentry,\n })\n}\n\n/**\n * Send a single event to Sentry as a structured log.\n *\n * @example\n * ```ts\n * await sendToSentry(event, {\n * dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendToSentry(event: WideEvent, config: SentryConfig): Promise<void> {\n await sendBatchToSentry([event], config)\n}\n\n/**\n * Send a batch of events to Sentry as structured logs via the Envelope endpoint.\n *\n * @example\n * ```ts\n * await sendBatchToSentry(events, {\n * dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendBatchToSentry(events: WideEvent[], config: SentryConfig): Promise<void> {\n if (events.length === 0) return\n\n const { url, authHeader } = getSentryEnvelopeUrl(config.dsn)\n\n const logs = events.map(event => toSentryLog(event, config))\n const body = buildEnvelopeBody(logs, config.dsn)\n\n await httpPost({\n url,\n headers: {\n 'Content-Type': 'application/x-sentry-envelope',\n 'X-Sentry-Auth': authHeader,\n },\n body,\n timeout: config.timeout ?? 5000,\n label: 'Sentry',\n })\n}\n"],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import { DrainContext } from "./types.mjs";
2
+ import { DrainPipelineOptions, PipelineDrainFn } from "./pipeline.mjs";
3
+
4
+ //#region src/browser.d.ts
5
+ interface BrowserDrainConfig {
6
+ /** URL of the server ingest endpoint */
7
+ endpoint: string;
8
+ /** Custom headers sent with each fetch request (e.g. Authorization, X-API-Key). Not applied to sendBeacon — see `useBeacon`. */
9
+ headers?: Record<string, string>;
10
+ /** Request timeout in milliseconds. @default 5000 */
11
+ timeout?: number;
12
+ /** Use sendBeacon when the page is hidden. @default true */
13
+ useBeacon?: boolean;
14
+ }
15
+ interface BrowserLogDrainOptions {
16
+ /** Browser drain configuration (endpoint is required) */
17
+ drain: BrowserDrainConfig;
18
+ /** Pipeline configuration overrides */
19
+ pipeline?: DrainPipelineOptions<DrainContext>;
20
+ /** Auto-register visibilitychange flush listener. @default true */
21
+ autoFlush?: boolean;
22
+ }
23
+ /**
24
+ * Create a low-level browser drain transport function.
25
+ *
26
+ * Returns a function compatible with `createDrainPipeline` that sends batches
27
+ * to the configured endpoint via `fetch` (with `keepalive: true`) or
28
+ * `navigator.sendBeacon` when the page is hidden.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { createBrowserDrain } from 'evlog/browser'
33
+ * import { createDrainPipeline } from 'evlog/pipeline'
34
+ *
35
+ * const pipeline = createDrainPipeline({ batch: { size: 50 } })
36
+ * const drain = pipeline(createBrowserDrain({ endpoint: '/api/logs' }))
37
+ * ```
38
+ */
39
+ declare function createBrowserDrain(config: BrowserDrainConfig): (batch: DrainContext[]) => Promise<void>;
40
+ /**
41
+ * Create a pre-composed browser log drain with pipeline, batching, and auto-flush.
42
+ *
43
+ * Returns a `PipelineDrainFn<DrainContext>` directly usable with `initLogger({ drain })`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import { initLogger, log } from 'evlog'
48
+ * import { createBrowserLogDrain } from 'evlog/browser'
49
+ *
50
+ * const drain = createBrowserLogDrain({
51
+ * drain: { endpoint: '/api/logs' },
52
+ * })
53
+ * initLogger({ drain })
54
+ *
55
+ * log.info({ action: 'page_view', path: location.pathname })
56
+ * ```
57
+ */
58
+ declare function createBrowserLogDrain(options: BrowserLogDrainOptions): PipelineDrainFn<DrainContext> & {
59
+ dispose: () => void;
60
+ };
61
+ //#endregion
62
+ export { BrowserDrainConfig, BrowserLogDrainOptions, createBrowserDrain, createBrowserLogDrain };
63
+ //# sourceMappingURL=browser.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.mts","names":[],"sources":["../src/browser.ts"],"mappings":";;;;UAIiB,kBAAA;;EAEf,QAAA;EAFiC;EAIjC,OAAA,GAAU,MAAA;EAAM;EAEhB,OAAA;EAFA;EAIA,SAAA;AAAA;AAAA,UAGe,sBAAA;EAHN;EAKT,KAAA,EAAO,kBAAA;EAFQ;EAIf,QAAA,GAAW,oBAAA,CAAqB,YAAA;;EAEhC,SAAA;AAAA;;;;;;;;;;;;AAmBF;;;;;iBAAgB,kBAAA,CAAmB,MAAA,EAAQ,kBAAA,IAAsB,KAAA,EAAO,YAAA,OAAmB,OAAA;;;;;;;;;;AA8D3F;;;;;;;;;iBAAgB,qBAAA,CAAsB,OAAA,EAAS,sBAAA,GAAyB,eAAA,CAAgB,YAAA;EAAkB,OAAA;AAAA"}
@@ -0,0 +1,95 @@
1
+ import { createDrainPipeline } from "./pipeline.mjs";
2
+
3
+ //#region src/browser.ts
4
+ /**
5
+ * Create a low-level browser drain transport function.
6
+ *
7
+ * Returns a function compatible with `createDrainPipeline` that sends batches
8
+ * to the configured endpoint via `fetch` (with `keepalive: true`) or
9
+ * `navigator.sendBeacon` when the page is hidden.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { createBrowserDrain } from 'evlog/browser'
14
+ * import { createDrainPipeline } from 'evlog/pipeline'
15
+ *
16
+ * const pipeline = createDrainPipeline({ batch: { size: 50 } })
17
+ * const drain = pipeline(createBrowserDrain({ endpoint: '/api/logs' }))
18
+ * ```
19
+ */
20
+ function createBrowserDrain(config) {
21
+ const { endpoint, headers: customHeaders, timeout = 5e3, useBeacon = true } = config;
22
+ return async (batch) => {
23
+ if (batch.length === 0) return;
24
+ const body = JSON.stringify(batch);
25
+ if (useBeacon && typeof document !== "undefined" && document.visibilityState === "hidden" && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
26
+ if (!navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }))) throw new Error("[evlog/browser] sendBeacon failed — payload may exceed browser limit");
27
+ return;
28
+ }
29
+ const controller = new AbortController();
30
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
31
+ try {
32
+ const response = await fetch(endpoint, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ ...customHeaders
37
+ },
38
+ body,
39
+ signal: controller.signal,
40
+ keepalive: true,
41
+ credentials: "same-origin"
42
+ });
43
+ if (!response.ok) throw new Error(`[evlog/browser] Server responded with ${response.status}`);
44
+ } finally {
45
+ clearTimeout(timeoutId);
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Create a pre-composed browser log drain with pipeline, batching, and auto-flush.
51
+ *
52
+ * Returns a `PipelineDrainFn<DrainContext>` directly usable with `initLogger({ drain })`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { initLogger, log } from 'evlog'
57
+ * import { createBrowserLogDrain } from 'evlog/browser'
58
+ *
59
+ * const drain = createBrowserLogDrain({
60
+ * drain: { endpoint: '/api/logs' },
61
+ * })
62
+ * initLogger({ drain })
63
+ *
64
+ * log.info({ action: 'page_view', path: location.pathname })
65
+ * ```
66
+ */
67
+ function createBrowserLogDrain(options) {
68
+ const { autoFlush = true } = options;
69
+ const drain = createDrainPipeline({
70
+ batch: {
71
+ size: 25,
72
+ intervalMs: 2e3
73
+ },
74
+ retry: { maxAttempts: 2 },
75
+ ...options.pipeline
76
+ })(createBrowserDrain(options.drain));
77
+ let onVisibilityChange;
78
+ if (autoFlush && typeof document !== "undefined") {
79
+ onVisibilityChange = () => {
80
+ if (document.visibilityState === "hidden") drain.flush();
81
+ };
82
+ document.addEventListener("visibilitychange", onVisibilityChange);
83
+ }
84
+ drain.dispose = () => {
85
+ if (onVisibilityChange) {
86
+ document.removeEventListener("visibilitychange", onVisibilityChange);
87
+ onVisibilityChange = void 0;
88
+ }
89
+ };
90
+ return drain;
91
+ }
92
+
93
+ //#endregion
94
+ export { createBrowserDrain, createBrowserLogDrain };
95
+ //# sourceMappingURL=browser.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["import type { DrainContext } from './types'\nimport type { DrainPipelineOptions, PipelineDrainFn } from './pipeline'\nimport { createDrainPipeline } from './pipeline'\n\nexport interface BrowserDrainConfig {\n /** URL of the server ingest endpoint */\n endpoint: string\n /** Custom headers sent with each fetch request (e.g. Authorization, X-API-Key). Not applied to sendBeacon — see `useBeacon`. */\n headers?: Record<string, string>\n /** Request timeout in milliseconds. @default 5000 */\n timeout?: number\n /** Use sendBeacon when the page is hidden. @default true */\n useBeacon?: boolean\n}\n\nexport interface BrowserLogDrainOptions {\n /** Browser drain configuration (endpoint is required) */\n drain: BrowserDrainConfig\n /** Pipeline configuration overrides */\n pipeline?: DrainPipelineOptions<DrainContext>\n /** Auto-register visibilitychange flush listener. @default true */\n autoFlush?: boolean\n}\n\n/**\n * Create a low-level browser drain transport function.\n *\n * Returns a function compatible with `createDrainPipeline` that sends batches\n * to the configured endpoint via `fetch` (with `keepalive: true`) or\n * `navigator.sendBeacon` when the page is hidden.\n *\n * @example\n * ```ts\n * import { createBrowserDrain } from 'evlog/browser'\n * import { createDrainPipeline } from 'evlog/pipeline'\n *\n * const pipeline = createDrainPipeline({ batch: { size: 50 } })\n * const drain = pipeline(createBrowserDrain({ endpoint: '/api/logs' }))\n * ```\n */\nexport function createBrowserDrain(config: BrowserDrainConfig): (batch: DrainContext[]) => Promise<void> {\n const { endpoint, headers: customHeaders, timeout = 5000, useBeacon = true } = config\n\n return async (batch: DrainContext[]): Promise<void> => {\n if (batch.length === 0) return\n\n const body = JSON.stringify(batch)\n\n if (\n useBeacon\n && typeof document !== 'undefined'\n && document.visibilityState === 'hidden'\n && typeof navigator !== 'undefined'\n && typeof navigator.sendBeacon === 'function'\n ) {\n const queued = navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }))\n if (!queued) {\n throw new Error('[evlog/browser] sendBeacon failed — payload may exceed browser limit')\n }\n return\n }\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body,\n signal: controller.signal,\n keepalive: true,\n credentials: 'same-origin',\n })\n\n if (!response.ok) {\n throw new Error(`[evlog/browser] Server responded with ${response.status}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n\n/**\n * Create a pre-composed browser log drain with pipeline, batching, and auto-flush.\n *\n * Returns a `PipelineDrainFn<DrainContext>` directly usable with `initLogger({ drain })`.\n *\n * @example\n * ```ts\n * import { initLogger, log } from 'evlog'\n * import { createBrowserLogDrain } from 'evlog/browser'\n *\n * const drain = createBrowserLogDrain({\n * drain: { endpoint: '/api/logs' },\n * })\n * initLogger({ drain })\n *\n * log.info({ action: 'page_view', path: location.pathname })\n * ```\n */\nexport function createBrowserLogDrain(options: BrowserLogDrainOptions): PipelineDrainFn<DrainContext> & { dispose: () => void } {\n const { autoFlush = true } = options\n\n const pipeline = createDrainPipeline<DrainContext>({\n batch: { size: 25, intervalMs: 2000 },\n retry: { maxAttempts: 2 },\n ...options.pipeline,\n })\n\n const drain = pipeline(createBrowserDrain(options.drain)) as PipelineDrainFn<DrainContext> & { dispose: () => void }\n\n let onVisibilityChange: (() => void) | undefined\n\n if (autoFlush && typeof document !== 'undefined') {\n onVisibilityChange = () => {\n if (document.visibilityState === 'hidden') {\n drain.flush()\n }\n }\n document.addEventListener('visibilitychange', onVisibilityChange)\n }\n\n drain.dispose = () => {\n if (onVisibilityChange) {\n document.removeEventListener('visibilitychange', onVisibilityChange)\n onVisibilityChange = undefined\n }\n }\n\n return drain\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,mBAAmB,QAAsE;CACvG,MAAM,EAAE,UAAU,SAAS,eAAe,UAAU,KAAM,YAAY,SAAS;AAE/E,QAAO,OAAO,UAAyC;AACrD,MAAI,MAAM,WAAW,EAAG;EAExB,MAAM,OAAO,KAAK,UAAU,MAAM;AAElC,MACE,aACG,OAAO,aAAa,eACpB,SAAS,oBAAoB,YAC7B,OAAO,cAAc,eACrB,OAAO,UAAU,eAAe,YACnC;AAEA,OAAI,CADW,UAAU,WAAW,UAAU,IAAI,KAAK,CAAC,KAAK,EAAE,EAAE,MAAM,oBAAoB,CAAC,CAAC,CAE3F,OAAM,IAAI,MAAM,uEAAuE;AAEzF;;EAGF,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,MAAI;GACF,MAAM,WAAW,MAAM,MAAM,UAAU;IACrC,QAAQ;IACR,SAAS;KAAE,gBAAgB;KAAoB,GAAG;KAAe;IACjE;IACA,QAAQ,WAAW;IACnB,WAAW;IACX,aAAa;IACd,CAAC;AAEF,OAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,yCAAyC,SAAS,SAAS;YAErE;AACR,gBAAa,UAAU;;;;;;;;;;;;;;;;;;;;;;AAuB7B,SAAgB,sBAAsB,SAA0F;CAC9H,MAAM,EAAE,YAAY,SAAS;CAQ7B,MAAM,QANW,oBAAkC;EACjD,OAAO;GAAE,MAAM;GAAI,YAAY;GAAM;EACrC,OAAO,EAAE,aAAa,GAAG;EACzB,GAAG,QAAQ;EACZ,CAAC,CAEqB,mBAAmB,QAAQ,MAAM,CAAC;CAEzD,IAAI;AAEJ,KAAI,aAAa,OAAO,aAAa,aAAa;AAChD,6BAA2B;AACzB,OAAI,SAAS,oBAAoB,SAC/B,OAAM,OAAO;;AAGjB,WAAS,iBAAiB,oBAAoB,mBAAmB;;AAGnE,OAAM,gBAAgB;AACpB,MAAI,oBAAoB;AACtB,YAAS,oBAAoB,oBAAoB,mBAAmB;AACpE,wBAAqB;;;AAIzB,QAAO"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BaseWideEvent, DeepPartial, DrainContext, EnrichContext, EnvironmentContext, ErrorOptions, FieldContext, H3EventContext, IngestPayload, InternalFields, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from "./types.mjs";
2
2
  import { EvlogError, createError } from "./error.mjs";
3
- import { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from "./logger.mjs";
3
+ import { createRequestLogger, getEnvironment, initLogger, isEnabled, log as _log, shouldKeep } from "./logger.mjs";
4
4
  import { useLogger } from "./runtime/server/useLogger.mjs";
5
5
  import { parseError } from "./runtime/utils/parseError.mjs";
6
- export { type BaseWideEvent, type DeepPartial, type DrainContext, type EnrichContext, type EnvironmentContext, type ErrorOptions, EvlogError, type FieldContext, type H3EventContext, type IngestPayload, type InternalFields, type Log, type LogLevel, type LoggerConfig, type ParsedError, type RequestLogger, type RequestLoggerOptions, type SamplingConfig, type SamplingRates, type ServerEvent, type TailSamplingCondition, type TailSamplingContext, type TransportConfig, type WideEvent, createError, createError as createEvlogError, createRequestLogger, getEnvironment, initLogger, log, parseError, shouldKeep, useLogger };
6
+ export { type BaseWideEvent, type DeepPartial, type DrainContext, type EnrichContext, type EnvironmentContext, type ErrorOptions, EvlogError, type FieldContext, type H3EventContext, type IngestPayload, type InternalFields, type Log, type LogLevel, type LoggerConfig, type ParsedError, type RequestLogger, type RequestLoggerOptions, type SamplingConfig, type SamplingRates, type ServerEvent, type TailSamplingCondition, type TailSamplingContext, type TransportConfig, type WideEvent, createError, createError as createEvlogError, createRequestLogger, getEnvironment, initLogger, isEnabled, _log as log, parseError, shouldKeep, useLogger };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EvlogError, createError } from "./error.mjs";
2
- import { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from "./logger.mjs";
2
+ import { createRequestLogger, getEnvironment, initLogger, isEnabled, log as _log, shouldKeep } from "./logger.mjs";
3
3
  import { useLogger } from "./runtime/server/useLogger.mjs";
4
4
  import { parseError } from "./runtime/utils/parseError.mjs";
5
5
 
6
- export { EvlogError, createError, createError as createEvlogError, createRequestLogger, getEnvironment, initLogger, log, parseError, shouldKeep, useLogger };
6
+ export { EvlogError, createError, createError as createEvlogError, createRequestLogger, getEnvironment, initLogger, isEnabled, _log as log, parseError, shouldKeep, useLogger };
package/dist/logger.d.mts CHANGED
@@ -6,6 +6,10 @@ import { EnvironmentContext, Log, LoggerConfig, RequestLogger, RequestLoggerOpti
6
6
  * Call this once at application startup.
7
7
  */
8
8
  declare function initLogger(config?: LoggerConfig): void;
9
+ /**
10
+ * Check if logging is globally enabled.
11
+ */
12
+ declare function isEnabled(): boolean;
9
13
  /**
10
14
  * Evaluate tail sampling conditions to determine if a log should be force-kept.
11
15
  * Returns true if ANY condition matches (OR logic).
@@ -20,7 +24,7 @@ declare function shouldKeep(ctx: TailSamplingContext): boolean;
20
24
  * log.error({ action: 'payment', error: 'failed' })
21
25
  * ```
22
26
  */
23
- declare const log: Log;
27
+ declare const _log: Log;
24
28
  /**
25
29
  * Create a request-scoped logger for building wide events.
26
30
  *
@@ -38,5 +42,5 @@ declare function createRequestLogger<T extends object = Record<string, unknown>>
38
42
  */
39
43
  declare function getEnvironment(): EnvironmentContext;
40
44
  //#endregion
41
- export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep };
45
+ export { createRequestLogger, getEnvironment, initLogger, isEnabled, _log as log, shouldKeep };
42
46
  //# sourceMappingURL=logger.d.mts.map