evlog 1.6.0 → 1.7.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 (104) hide show
  1. package/README.md +95 -0
  2. package/dist/_utils-DZA9nou3.mjs +23 -0
  3. package/dist/_utils-DZA9nou3.mjs.map +1 -0
  4. package/dist/adapters/axiom.d.mts +16 -15
  5. package/dist/adapters/axiom.d.mts.map +1 -0
  6. package/dist/adapters/axiom.mjs +95 -50
  7. package/dist/adapters/axiom.mjs.map +1 -0
  8. package/dist/adapters/better-stack.d.mts +62 -0
  9. package/dist/adapters/better-stack.d.mts.map +1 -0
  10. package/dist/adapters/better-stack.mjs +109 -0
  11. package/dist/adapters/better-stack.mjs.map +1 -0
  12. package/dist/adapters/otlp.d.mts +31 -30
  13. package/dist/adapters/otlp.d.mts.map +1 -0
  14. package/dist/adapters/otlp.mjs +197 -177
  15. package/dist/adapters/otlp.mjs.map +1 -0
  16. package/dist/adapters/posthog.d.mts +20 -19
  17. package/dist/adapters/posthog.d.mts.map +1 -0
  18. package/dist/adapters/posthog.mjs +109 -62
  19. package/dist/adapters/posthog.mjs.map +1 -0
  20. package/dist/adapters/sentry.d.mts +24 -23
  21. package/dist/adapters/sentry.d.mts.map +1 -0
  22. package/dist/adapters/sentry.mjs +208 -151
  23. package/dist/adapters/sentry.mjs.map +1 -0
  24. package/dist/enrichers.d.mts +74 -0
  25. package/dist/enrichers.d.mts.map +1 -0
  26. package/dist/enrichers.mjs +172 -0
  27. package/dist/enrichers.mjs.map +1 -0
  28. package/dist/error.d.mts +24 -22
  29. package/dist/error.d.mts.map +1 -0
  30. package/dist/error.mjs +107 -76
  31. package/dist/error.mjs.map +1 -0
  32. package/dist/index.d.mts +6 -5
  33. package/dist/index.mjs +6 -5
  34. package/dist/logger.d.mts +5 -3
  35. package/dist/logger.d.mts.map +1 -0
  36. package/dist/logger.mjs +200 -184
  37. package/dist/logger.mjs.map +1 -0
  38. package/dist/nitro/errorHandler.d.mts +4 -2
  39. package/dist/nitro/errorHandler.d.mts.map +1 -0
  40. package/dist/nitro/errorHandler.mjs +49 -38
  41. package/dist/nitro/errorHandler.mjs.map +1 -0
  42. package/dist/nitro/plugin.d.mts +4 -2
  43. package/dist/nitro/plugin.d.mts.map +1 -0
  44. package/dist/nitro/plugin.mjs +155 -136
  45. package/dist/nitro/plugin.mjs.map +1 -0
  46. package/dist/nuxt/module.d.mts +149 -168
  47. package/dist/nuxt/module.d.mts.map +1 -0
  48. package/dist/nuxt/module.mjs +66 -65
  49. package/dist/nuxt/module.mjs.map +1 -0
  50. package/dist/pipeline.d.mts +46 -0
  51. package/dist/pipeline.d.mts.map +1 -0
  52. package/dist/pipeline.mjs +122 -0
  53. package/dist/pipeline.mjs.map +1 -0
  54. package/dist/runtime/client/log.d.mts +7 -5
  55. package/dist/runtime/client/log.d.mts.map +1 -0
  56. package/dist/runtime/client/log.mjs +57 -62
  57. package/dist/runtime/client/log.mjs.map +1 -0
  58. package/dist/runtime/client/plugin.d.mts +3 -1
  59. package/dist/runtime/client/plugin.d.mts.map +1 -0
  60. package/dist/runtime/client/plugin.mjs +13 -12
  61. package/dist/runtime/client/plugin.mjs.map +1 -0
  62. package/dist/runtime/server/routes/_evlog/ingest.post.d.mts +4 -2
  63. package/dist/runtime/server/routes/_evlog/ingest.post.d.mts.map +1 -0
  64. package/dist/runtime/server/routes/_evlog/ingest.post.mjs +113 -76
  65. package/dist/runtime/server/routes/_evlog/ingest.post.mjs.map +1 -0
  66. package/dist/runtime/server/useLogger.d.mts +14 -3
  67. package/dist/runtime/server/useLogger.d.mts.map +1 -0
  68. package/dist/runtime/server/useLogger.mjs +39 -10
  69. package/dist/runtime/server/useLogger.mjs.map +1 -0
  70. package/dist/runtime/utils/parseError.d.mts +5 -3
  71. package/dist/runtime/utils/parseError.d.mts.map +1 -0
  72. package/dist/runtime/utils/parseError.mjs +25 -26
  73. package/dist/runtime/utils/parseError.mjs.map +1 -0
  74. package/dist/types.d.mts +348 -246
  75. package/dist/types.d.mts.map +1 -0
  76. package/dist/types.mjs +1 -1
  77. package/dist/utils.d.mts +19 -14
  78. package/dist/utils.d.mts.map +1 -0
  79. package/dist/utils.mjs +59 -50
  80. package/dist/utils.mjs.map +1 -0
  81. package/dist/workers.d.mts +10 -9
  82. package/dist/workers.d.mts.map +1 -0
  83. package/dist/workers.mjs +68 -39
  84. package/dist/workers.mjs.map +1 -0
  85. package/package.json +26 -5
  86. package/dist/adapters/axiom.d.ts +0 -62
  87. package/dist/adapters/otlp.d.ts +0 -83
  88. package/dist/adapters/posthog.d.ts +0 -72
  89. package/dist/adapters/sentry.d.ts +0 -78
  90. package/dist/error.d.ts +0 -63
  91. package/dist/index.d.ts +0 -5
  92. package/dist/logger.d.ts +0 -40
  93. package/dist/nitro/errorHandler.d.ts +0 -13
  94. package/dist/nitro/plugin.d.ts +0 -5
  95. package/dist/nuxt/module.d.ts +0 -171
  96. package/dist/runtime/client/log.d.ts +0 -10
  97. package/dist/runtime/client/plugin.d.ts +0 -3
  98. package/dist/runtime/server/routes/_evlog/ingest.post.d.ts +0 -5
  99. package/dist/runtime/server/useLogger.d.ts +0 -28
  100. package/dist/runtime/utils/parseError.d.ts +0 -5
  101. package/dist/shared/evlog.Bc35pxiY.mjs +0 -10
  102. package/dist/types.d.ts +0 -364
  103. package/dist/utils.d.ts +0 -29
  104. package/dist/workers.d.ts +0 -45
@@ -1,176 +1,233 @@
1
- import { g as getRuntimeConfig } from '../shared/evlog.Bc35pxiY.mjs';
1
+ import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
2
2
 
3
+ //#region src/adapters/sentry.ts
4
+ /** Based on OpenTelemetry Logs Data Model specification */
3
5
  const SEVERITY_MAP = {
4
- debug: 5,
5
- info: 9,
6
- warn: 13,
7
- error: 17
6
+ debug: 5,
7
+ info: 9,
8
+ warn: 13,
9
+ error: 17
8
10
  };
9
11
  function parseSentryDsn(dsn) {
10
- const url = new URL(dsn);
11
- const publicKey = url.username;
12
- if (!publicKey) {
13
- throw new Error("Invalid Sentry DSN: missing public key");
14
- }
15
- const secretKey = url.password || void 0;
16
- const pathParts = url.pathname.split("/").filter(Boolean);
17
- const projectId = pathParts.pop();
18
- if (!projectId) {
19
- throw new Error("Invalid Sentry DSN: missing project ID");
20
- }
21
- const basePath = pathParts.length > 0 ? `/${pathParts.join("/")}` : "";
22
- return {
23
- publicKey,
24
- secretKey,
25
- projectId,
26
- origin: `${url.protocol}//${url.host}`,
27
- basePath
28
- };
12
+ const url = new URL(dsn);
13
+ const publicKey = url.username;
14
+ if (!publicKey) throw new Error("Invalid Sentry DSN: missing public key");
15
+ const secretKey = url.password || void 0;
16
+ const pathParts = url.pathname.split("/").filter(Boolean);
17
+ const projectId = pathParts.pop();
18
+ if (!projectId) throw new Error("Invalid Sentry DSN: missing project ID");
19
+ const basePath = pathParts.length > 0 ? `/${pathParts.join("/")}` : "";
20
+ return {
21
+ publicKey,
22
+ secretKey,
23
+ projectId,
24
+ origin: `${url.protocol}//${url.host}`,
25
+ basePath
26
+ };
29
27
  }
30
28
  function getSentryEnvelopeUrl(dsn) {
31
- const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn);
32
- const url = `${origin}${basePath}/api/${projectId}/envelope/`;
33
- let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=evlog`;
34
- if (secretKey) {
35
- authHeader += `, sentry_secret=${secretKey}`;
36
- }
37
- return { url, authHeader };
29
+ const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn);
30
+ const url = `${origin}${basePath}/api/${projectId}/envelope/`;
31
+ let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=evlog`;
32
+ if (secretKey) authHeader += `, sentry_secret=${secretKey}`;
33
+ return {
34
+ url,
35
+ authHeader
36
+ };
38
37
  }
39
38
  function createTraceId() {
40
- if (typeof globalThis.crypto?.randomUUID === "function") {
41
- return globalThis.crypto.randomUUID().replace(/-/g, "");
42
- }
43
- return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
39
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID().replace(/-/g, "");
40
+ return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
44
41
  }
45
42
  function getFirstStringValue(event, keys) {
46
- for (const key of keys) {
47
- const value = event[key];
48
- if (typeof value === "string" && value.length > 0) return value;
49
- }
50
- return void 0;
43
+ for (const key of keys) {
44
+ const value = event[key];
45
+ if (typeof value === "string" && value.length > 0) return value;
46
+ }
51
47
  }
52
48
  function toAttributeValue(value) {
53
- if (value === null || value === void 0) {
54
- return void 0;
55
- }
56
- if (typeof value === "string") {
57
- return { value, type: "string" };
58
- }
59
- if (typeof value === "boolean") {
60
- return { value, type: "boolean" };
61
- }
62
- if (typeof value === "number") {
63
- if (Number.isInteger(value)) {
64
- return { value, type: "integer" };
65
- }
66
- return { value, type: "double" };
67
- }
68
- return { value: JSON.stringify(value), type: "string" };
49
+ if (value === null || value === void 0) return;
50
+ if (typeof value === "string") return {
51
+ value,
52
+ type: "string"
53
+ };
54
+ if (typeof value === "boolean") return {
55
+ value,
56
+ type: "boolean"
57
+ };
58
+ if (typeof value === "number") {
59
+ if (Number.isInteger(value)) return {
60
+ value,
61
+ type: "integer"
62
+ };
63
+ return {
64
+ value,
65
+ type: "double"
66
+ };
67
+ }
68
+ return {
69
+ value: JSON.stringify(value),
70
+ type: "string"
71
+ };
69
72
  }
70
73
  function toSentryLog(event, config) {
71
- const { timestamp, level, service, environment, version, ...rest } = event;
72
- const body = getFirstStringValue(event, ["message", "action", "path"]) ?? "evlog wide event";
73
- const traceId = typeof event.traceId === "string" && event.traceId.length > 0 ? event.traceId : createTraceId();
74
- const attributes = {};
75
- const env = config.environment ?? environment;
76
- if (env) {
77
- attributes["sentry.environment"] = { value: env, type: "string" };
78
- }
79
- const rel = config.release ?? version;
80
- if (typeof rel === "string" && rel.length > 0) {
81
- attributes["sentry.release"] = { value: rel, type: "string" };
82
- }
83
- attributes["service"] = { value: service, type: "string" };
84
- if (config.tags) {
85
- for (const [key, value] of Object.entries(config.tags)) {
86
- attributes[key] = { value, type: "string" };
87
- }
88
- }
89
- for (const [key, value] of Object.entries(rest)) {
90
- if (key === "traceId" || key === "spanId") continue;
91
- if (value === void 0 || value === null) continue;
92
- const attr = toAttributeValue(value);
93
- if (attr) {
94
- attributes[key] = attr;
95
- }
96
- }
97
- return {
98
- timestamp: new Date(timestamp).getTime() / 1e3,
99
- trace_id: traceId,
100
- level,
101
- body,
102
- severity_number: SEVERITY_MAP[level] ?? 9,
103
- attributes
104
- };
74
+ const { timestamp, level, service, environment, version, ...rest } = event;
75
+ const body = getFirstStringValue(event, [
76
+ "message",
77
+ "action",
78
+ "path"
79
+ ]) ?? "evlog wide event";
80
+ const traceId = typeof event.traceId === "string" && event.traceId.length > 0 ? event.traceId : createTraceId();
81
+ const attributes = {};
82
+ const env = config.environment ?? environment;
83
+ if (env) attributes["sentry.environment"] = {
84
+ value: env,
85
+ type: "string"
86
+ };
87
+ const rel = config.release ?? version;
88
+ if (typeof rel === "string" && rel.length > 0) attributes["sentry.release"] = {
89
+ value: rel,
90
+ type: "string"
91
+ };
92
+ attributes["service"] = {
93
+ value: service,
94
+ type: "string"
95
+ };
96
+ if (config.tags) for (const [key, value] of Object.entries(config.tags)) attributes[key] = {
97
+ value,
98
+ type: "string"
99
+ };
100
+ for (const [key, value] of Object.entries(rest)) {
101
+ if (key === "traceId" || key === "spanId") continue;
102
+ if (value === void 0 || value === null) continue;
103
+ const attr = toAttributeValue(value);
104
+ if (attr) attributes[key] = attr;
105
+ }
106
+ return {
107
+ timestamp: new Date(timestamp).getTime() / 1e3,
108
+ trace_id: traceId,
109
+ level,
110
+ body,
111
+ severity_number: SEVERITY_MAP[level] ?? 9,
112
+ attributes
113
+ };
105
114
  }
115
+ /**
116
+ * Build the Sentry Envelope body for a list of logs.
117
+ *
118
+ * Envelope format (line-delimited):
119
+ * - Line 1: Envelope headers (dsn, sent_at)
120
+ * - Line 2: Item header (type: log, item_count, content_type)
121
+ * - Line 3: Item payload ({"items": [...]})
122
+ */
106
123
  function buildEnvelopeBody(logs, dsn) {
107
- const envelopeHeader = JSON.stringify({
108
- dsn,
109
- sent_at: (/* @__PURE__ */ new Date()).toISOString()
110
- });
111
- const itemHeader = JSON.stringify({
112
- type: "log",
113
- item_count: logs.length,
114
- content_type: "application/vnd.sentry.items.log+json"
115
- });
116
- const itemPayload = JSON.stringify({ items: logs });
117
- return `${envelopeHeader}
118
- ${itemHeader}
119
- ${itemPayload}
120
- `;
124
+ return `${JSON.stringify({
125
+ dsn,
126
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
127
+ })}\n${JSON.stringify({
128
+ type: "log",
129
+ item_count: logs.length,
130
+ content_type: "application/vnd.sentry.items.log+json"
131
+ })}\n${JSON.stringify({ items: logs })}\n`;
121
132
  }
133
+ /**
134
+ * Create a drain function for sending logs to Sentry.
135
+ *
136
+ * Sends wide events as Sentry Structured Logs, visible in Explore > Logs
137
+ * in the Sentry dashboard.
138
+ *
139
+ * Configuration priority (highest to lowest):
140
+ * 1. Overrides passed to createSentryDrain()
141
+ * 2. runtimeConfig.evlog.sentry
142
+ * 3. runtimeConfig.sentry
143
+ * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * // Zero config - just set NUXT_SENTRY_DSN env var
148
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain())
149
+ *
150
+ * // With overrides
151
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain({
152
+ * dsn: 'https://public@o0.ingest.sentry.io/123',
153
+ * }))
154
+ * ```
155
+ */
122
156
  function createSentryDrain(overrides) {
123
- return async (ctx) => {
124
- const runtimeConfig = getRuntimeConfig();
125
- const evlogSentry = runtimeConfig?.evlog?.sentry;
126
- const rootSentry = runtimeConfig?.sentry;
127
- const config = {
128
- dsn: overrides?.dsn ?? evlogSentry?.dsn ?? rootSentry?.dsn ?? process.env.NUXT_SENTRY_DSN ?? process.env.SENTRY_DSN,
129
- environment: overrides?.environment ?? evlogSentry?.environment ?? rootSentry?.environment ?? process.env.NUXT_SENTRY_ENVIRONMENT ?? process.env.SENTRY_ENVIRONMENT,
130
- release: overrides?.release ?? evlogSentry?.release ?? rootSentry?.release ?? process.env.NUXT_SENTRY_RELEASE ?? process.env.SENTRY_RELEASE,
131
- tags: overrides?.tags ?? evlogSentry?.tags ?? rootSentry?.tags,
132
- timeout: overrides?.timeout ?? evlogSentry?.timeout ?? rootSentry?.timeout
133
- };
134
- if (!config.dsn) {
135
- console.error("[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()");
136
- return;
137
- }
138
- try {
139
- await sendToSentry(ctx.event, config);
140
- } catch (error) {
141
- console.error("[evlog/sentry] Failed to send log:", error);
142
- }
143
- };
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
+ };
144
180
  }
181
+ /**
182
+ * Send a single event to Sentry as a structured log.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * await sendToSentry(event, {
187
+ * dsn: process.env.SENTRY_DSN!,
188
+ * })
189
+ * ```
190
+ */
145
191
  async function sendToSentry(event, config) {
146
- await sendBatchToSentry([event], config);
192
+ await sendBatchToSentry([event], config);
147
193
  }
194
+ /**
195
+ * Send a batch of events to Sentry as structured logs via the Envelope endpoint.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * await sendBatchToSentry(events, {
200
+ * dsn: process.env.SENTRY_DSN!,
201
+ * })
202
+ * ```
203
+ */
148
204
  async function sendBatchToSentry(events, config) {
149
- if (events.length === 0) return;
150
- const { url, authHeader } = getSentryEnvelopeUrl(config.dsn);
151
- const timeout = config.timeout ?? 5e3;
152
- const logs = events.map((event) => toSentryLog(event, config));
153
- const body = buildEnvelopeBody(logs, config.dsn);
154
- const controller = new AbortController();
155
- const timeoutId = setTimeout(() => controller.abort(), timeout);
156
- try {
157
- const response = await fetch(url, {
158
- method: "POST",
159
- headers: {
160
- "Content-Type": "application/x-sentry-envelope",
161
- "X-Sentry-Auth": authHeader
162
- },
163
- body,
164
- signal: controller.signal
165
- });
166
- if (!response.ok) {
167
- const text = await response.text().catch(() => "Unknown error");
168
- const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
169
- throw new Error(`Sentry API error: ${response.status} ${response.statusText} - ${safeText}`);
170
- }
171
- } finally {
172
- clearTimeout(timeoutId);
173
- }
205
+ if (events.length === 0) return;
206
+ const { url, authHeader } = getSentryEnvelopeUrl(config.dsn);
207
+ const timeout = config.timeout ?? 5e3;
208
+ 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
+ }
174
229
  }
175
230
 
231
+ //#endregion
176
232
  export { createSentryDrain, sendBatchToSentry, sendToSentry, toSentryLog };
233
+ //# sourceMappingURL=sentry.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,74 @@
1
+ import { EnrichContext } from "./types.mjs";
2
+
3
+ //#region src/enrichers/index.d.ts
4
+ interface EnricherOptions {
5
+ /**
6
+ * When true, overwrite any existing fields in the event.
7
+ * Defaults to false to preserve user-provided data.
8
+ */
9
+ overwrite?: boolean;
10
+ }
11
+ interface UserAgentInfo {
12
+ raw: string;
13
+ browser?: {
14
+ name: string;
15
+ version?: string;
16
+ };
17
+ os?: {
18
+ name: string;
19
+ version?: string;
20
+ };
21
+ device?: {
22
+ type: 'mobile' | 'tablet' | 'desktop' | 'bot' | 'unknown';
23
+ };
24
+ }
25
+ interface GeoInfo {
26
+ country?: string;
27
+ region?: string;
28
+ regionCode?: string;
29
+ city?: string;
30
+ latitude?: number;
31
+ longitude?: number;
32
+ }
33
+ interface RequestSizeInfo {
34
+ requestBytes?: number;
35
+ responseBytes?: number;
36
+ }
37
+ interface TraceContextInfo {
38
+ traceparent?: string;
39
+ tracestate?: string;
40
+ traceId?: string;
41
+ spanId?: string;
42
+ }
43
+ /**
44
+ * Enrich events with parsed user agent data.
45
+ * Sets `event.userAgent` with `UserAgentInfo` shape: `{ raw, browser?, os?, device? }`.
46
+ */
47
+ declare function createUserAgentEnricher(options?: EnricherOptions): (ctx: EnrichContext) => void;
48
+ /**
49
+ * Enrich events with geo data from platform headers.
50
+ * Sets `event.geo` with `GeoInfo` shape: `{ country?, region?, regionCode?, city?, latitude?, longitude? }`.
51
+ *
52
+ * Supports Vercel (`x-vercel-ip-*`) headers out of the box.
53
+ *
54
+ * **Cloudflare note:** Only `cf-ipcountry` is an actual HTTP header added by Cloudflare.
55
+ * The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard
56
+ * Cloudflare headers — they are properties of `request.cf` which is not exposed as HTTP
57
+ * headers. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`
58
+ * or use a Workers middleware to copy `cf` properties into custom headers.
59
+ */
60
+ declare function createGeoEnricher(options?: EnricherOptions): (ctx: EnrichContext) => void;
61
+ /**
62
+ * Enrich events with request/response payload sizes.
63
+ * Sets `event.requestSize` with `RequestSizeInfo` shape: `{ requestBytes?, responseBytes? }`.
64
+ */
65
+ declare function createRequestSizeEnricher(options?: EnricherOptions): (ctx: EnrichContext) => void;
66
+ /**
67
+ * Enrich events with W3C trace context data.
68
+ * Sets `event.traceContext` with `TraceContextInfo` shape: `{ traceparent?, tracestate?, traceId?, spanId? }`.
69
+ * Also sets `event.traceId` and `event.spanId` at the top level.
70
+ */
71
+ declare function createTraceContextEnricher(options?: EnricherOptions): (ctx: EnrichContext) => void;
72
+ //#endregion
73
+ export { EnricherOptions, GeoInfo, RequestSizeInfo, TraceContextInfo, UserAgentInfo, createGeoEnricher, createRequestSizeEnricher, createTraceContextEnricher, createUserAgentEnricher };
74
+ //# sourceMappingURL=enrichers.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enrichers.d.mts","names":[],"sources":["../src/enrichers/index.ts"],"mappings":";;;UAEiB,eAAA;;AAAjB;;;EAKE,SAAA;AAAA;AAAA,UAGe,aAAA;EACf,GAAA;EACA,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EAC1B,EAAA;IAAO,IAAA;IAAc,OAAA;EAAA;EACrB,MAAA;IAAW,IAAA;EAAA;AAAA;AAAA,UAGI,OAAA;EACf,OAAA;EACA,MAAA;EACA,UAAA;EACA,IAAA;EACA,QAAA;EACA,SAAA;AAAA;AAAA,UAGe,eAAA;EACf,YAAA;EACA,aAAA;AAAA;AAAA,UAGe,gBAAA;EACf,WAAA;EACA,UAAA;EACA,OAAA;EACA,MAAA;AAAA;AAJF;;;;AAAA,iBAoGgB,uBAAA,CAAwB,OAAA,GAAS,eAAA,IAAwB,GAAA,EAAK,aAAA;;;;;;AAA9E;;;;;;;iBAqBgB,iBAAA,CAAkB,OAAA,GAAS,eAAA,IAAwB,GAAA,EAAK,aAAA;;;AAAxE;;iBAuBgB,yBAAA,CAA0B,OAAA,GAAS,eAAA,IAAwB,GAAA,EAAK,aAAA;;;;;;iBAoBhE,0BAAA,CAA2B,OAAA,GAAS,eAAA,IAAwB,GAAA,EAAK,aAAA"}