evlog 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/_http-DVDwNag0.mjs +76 -0
- package/dist/_http-DVDwNag0.mjs.map +1 -0
- package/dist/_severity-CXfyvxQi.mjs +17 -0
- package/dist/_severity-CXfyvxQi.mjs.map +1 -0
- package/dist/adapters/axiom.d.mts +17 -15
- package/dist/adapters/axiom.d.mts.map +1 -0
- package/dist/adapters/axiom.mjs +91 -50
- package/dist/adapters/axiom.mjs.map +1 -0
- package/dist/adapters/better-stack.d.mts +63 -0
- package/dist/adapters/better-stack.d.mts.map +1 -0
- package/dist/adapters/better-stack.mjs +98 -0
- package/dist/adapters/better-stack.mjs.map +1 -0
- package/dist/adapters/otlp.d.mts +32 -30
- package/dist/adapters/otlp.d.mts.map +1 -0
- package/dist/adapters/otlp.mjs +181 -181
- package/dist/adapters/otlp.mjs.map +1 -0
- package/dist/adapters/posthog.d.mts +54 -19
- package/dist/adapters/posthog.d.mts.map +1 -0
- package/dist/adapters/posthog.mjs +156 -63
- package/dist/adapters/posthog.mjs.map +1 -0
- package/dist/adapters/sentry.d.mts +25 -23
- package/dist/adapters/sentry.d.mts.map +1 -0
- package/dist/adapters/sentry.mjs +198 -153
- package/dist/adapters/sentry.mjs.map +1 -0
- package/dist/browser.d.mts +63 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +95 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/enrichers.d.mts +74 -0
- package/dist/enrichers.d.mts.map +1 -0
- package/dist/enrichers.mjs +172 -0
- package/dist/enrichers.mjs.map +1 -0
- package/dist/error.d.mts +24 -22
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +107 -76
- package/dist/error.mjs.map +1 -0
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/logger.d.mts +11 -5
- package/dist/logger.d.mts.map +1 -0
- package/dist/logger.mjs +255 -186
- package/dist/logger.mjs.map +1 -0
- package/dist/nitro/errorHandler.d.mts +4 -2
- package/dist/nitro/errorHandler.d.mts.map +1 -0
- package/dist/nitro/errorHandler.mjs +38 -38
- package/dist/nitro/errorHandler.mjs.map +1 -0
- package/dist/nitro/module.d.mts +11 -0
- package/dist/nitro/module.d.mts.map +1 -0
- package/dist/nitro/module.mjs +23 -0
- package/dist/nitro/module.mjs.map +1 -0
- package/dist/nitro/plugin.d.mts +4 -2
- package/dist/nitro/plugin.d.mts.map +1 -0
- package/dist/nitro/plugin.mjs +135 -140
- package/dist/nitro/plugin.mjs.map +1 -0
- package/dist/nitro/v3/errorHandler.d.mts +24 -0
- package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
- package/dist/nitro/v3/errorHandler.mjs +36 -0
- package/dist/nitro/v3/errorHandler.mjs.map +1 -0
- package/dist/nitro/v3/index.d.mts +4 -0
- package/dist/nitro/v3/index.mjs +4 -0
- package/dist/nitro/v3/module.d.mts +10 -0
- package/dist/nitro/v3/module.d.mts.map +1 -0
- package/dist/nitro/v3/module.mjs +22 -0
- package/dist/nitro/v3/module.mjs.map +1 -0
- package/dist/nitro/v3/plugin.d.mts +14 -0
- package/dist/nitro/v3/plugin.d.mts.map +1 -0
- package/dist/nitro/v3/plugin.mjs +157 -0
- package/dist/nitro/v3/plugin.mjs.map +1 -0
- package/dist/nitro/v3/useLogger.d.mts +24 -0
- package/dist/nitro/v3/useLogger.d.mts.map +1 -0
- package/dist/nitro/v3/useLogger.mjs +27 -0
- package/dist/nitro/v3/useLogger.mjs.map +1 -0
- package/dist/nitro-D57TWGyN.mjs +73 -0
- package/dist/nitro-D57TWGyN.mjs.map +1 -0
- package/dist/nitro-D81NBVPi.d.mts +42 -0
- package/dist/nitro-D81NBVPi.d.mts.map +1 -0
- package/dist/nuxt/module.d.mts +155 -168
- package/dist/nuxt/module.d.mts.map +1 -0
- package/dist/nuxt/module.mjs +75 -65
- package/dist/nuxt/module.mjs.map +1 -0
- package/dist/pipeline.d.mts +46 -0
- package/dist/pipeline.d.mts.map +1 -0
- package/dist/pipeline.mjs +122 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/runtime/client/log.d.mts +12 -7
- package/dist/runtime/client/log.d.mts.map +1 -0
- package/dist/runtime/client/log.mjs +72 -64
- package/dist/runtime/client/log.mjs.map +1 -0
- package/dist/runtime/client/plugin.d.mts +3 -1
- package/dist/runtime/client/plugin.d.mts.map +1 -0
- package/dist/runtime/client/plugin.mjs +14 -12
- package/dist/runtime/client/plugin.mjs.map +1 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.d.mts +4 -2
- package/dist/runtime/server/routes/_evlog/ingest.post.d.mts.map +1 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +113 -76
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs.map +1 -0
- package/dist/runtime/server/useLogger.d.mts +14 -3
- package/dist/runtime/server/useLogger.d.mts.map +1 -0
- package/dist/runtime/server/useLogger.mjs +39 -10
- package/dist/runtime/server/useLogger.mjs.map +1 -0
- package/dist/runtime/utils/parseError.d.mts +5 -3
- package/dist/runtime/utils/parseError.d.mts.map +1 -0
- package/dist/runtime/utils/parseError.mjs +25 -26
- package/dist/runtime/utils/parseError.mjs.map +1 -0
- package/dist/types.d.mts +378 -246
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +1 -1
- package/dist/utils.d.mts +19 -14
- package/dist/utils.d.mts.map +1 -0
- package/dist/utils.mjs +59 -50
- package/dist/utils.mjs.map +1 -0
- package/dist/workers.d.mts +10 -9
- package/dist/workers.d.mts.map +1 -0
- package/dist/workers.mjs +68 -39
- package/dist/workers.mjs.map +1 -0
- package/package.json +55 -10
- package/dist/adapters/axiom.d.ts +0 -62
- package/dist/adapters/otlp.d.ts +0 -83
- package/dist/adapters/posthog.d.ts +0 -72
- package/dist/adapters/sentry.d.ts +0 -78
- package/dist/error.d.ts +0 -63
- package/dist/index.d.ts +0 -5
- package/dist/logger.d.ts +0 -40
- package/dist/nitro/errorHandler.d.ts +0 -13
- package/dist/nitro/plugin.d.ts +0 -5
- package/dist/nuxt/module.d.ts +0 -171
- package/dist/runtime/client/log.d.ts +0 -10
- package/dist/runtime/client/plugin.d.ts +0 -3
- package/dist/runtime/server/routes/_evlog/ingest.post.d.ts +0 -5
- package/dist/runtime/server/useLogger.d.ts +0 -28
- package/dist/runtime/utils/parseError.d.ts +0 -5
- package/dist/shared/evlog.Bc35pxiY.mjs +0 -10
- package/dist/types.d.ts +0 -364
- package/dist/utils.d.ts +0 -29
- package/dist/workers.d.ts +0 -45
package/dist/adapters/sentry.mjs
CHANGED
|
@@ -1,176 +1,221 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
4
|
+
//#region src/adapters/sentry.ts
|
|
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
|
+
];
|
|
9
21
|
function parseSentryDsn(dsn) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
projectId,
|
|
26
|
-
origin: `${url.protocol}//${url.host}`,
|
|
27
|
-
basePath
|
|
28
|
-
};
|
|
22
|
+
const url = new URL(dsn);
|
|
23
|
+
const publicKey = url.username;
|
|
24
|
+
if (!publicKey) throw new Error("Invalid Sentry DSN: missing public key");
|
|
25
|
+
const secretKey = url.password || void 0;
|
|
26
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
27
|
+
const projectId = pathParts.pop();
|
|
28
|
+
if (!projectId) throw new Error("Invalid Sentry DSN: missing project ID");
|
|
29
|
+
const basePath = pathParts.length > 0 ? `/${pathParts.join("/")}` : "";
|
|
30
|
+
return {
|
|
31
|
+
publicKey,
|
|
32
|
+
secretKey,
|
|
33
|
+
projectId,
|
|
34
|
+
origin: `${url.protocol}//${url.host}`,
|
|
35
|
+
basePath
|
|
36
|
+
};
|
|
29
37
|
}
|
|
30
38
|
function getSentryEnvelopeUrl(dsn) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn);
|
|
40
|
+
const url = `${origin}${basePath}/api/${projectId}/envelope/`;
|
|
41
|
+
let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=evlog`;
|
|
42
|
+
if (secretKey) authHeader += `, sentry_secret=${secretKey}`;
|
|
43
|
+
return {
|
|
44
|
+
url,
|
|
45
|
+
authHeader
|
|
46
|
+
};
|
|
38
47
|
}
|
|
39
48
|
function createTraceId() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
49
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID().replace(/-/g, "");
|
|
50
|
+
return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
44
51
|
}
|
|
45
52
|
function getFirstStringValue(event, keys) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return void 0;
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const value = event[key];
|
|
55
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
function toAttributeValue(value) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
if (value === null || value === void 0) return;
|
|
60
|
+
if (typeof value === "string") return {
|
|
61
|
+
value,
|
|
62
|
+
type: "string"
|
|
63
|
+
};
|
|
64
|
+
if (typeof value === "boolean") return {
|
|
65
|
+
value,
|
|
66
|
+
type: "boolean"
|
|
67
|
+
};
|
|
68
|
+
if (typeof value === "number") {
|
|
69
|
+
if (Number.isInteger(value)) return {
|
|
70
|
+
value,
|
|
71
|
+
type: "integer"
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
value,
|
|
75
|
+
type: "double"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
value: JSON.stringify(value),
|
|
80
|
+
type: "string"
|
|
81
|
+
};
|
|
69
82
|
}
|
|
70
83
|
function toSentryLog(event, config) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
84
|
+
const { timestamp, level, service, environment, version, ...rest } = event;
|
|
85
|
+
const body = getFirstStringValue(event, [
|
|
86
|
+
"message",
|
|
87
|
+
"action",
|
|
88
|
+
"path"
|
|
89
|
+
]) ?? "evlog wide event";
|
|
90
|
+
const traceId = typeof event.traceId === "string" && event.traceId.length > 0 ? event.traceId : createTraceId();
|
|
91
|
+
const attributes = {};
|
|
92
|
+
const env = config.environment ?? environment;
|
|
93
|
+
if (env) attributes["sentry.environment"] = {
|
|
94
|
+
value: env,
|
|
95
|
+
type: "string"
|
|
96
|
+
};
|
|
97
|
+
const rel = config.release ?? version;
|
|
98
|
+
if (typeof rel === "string" && rel.length > 0) attributes["sentry.release"] = {
|
|
99
|
+
value: rel,
|
|
100
|
+
type: "string"
|
|
101
|
+
};
|
|
102
|
+
attributes["service"] = {
|
|
103
|
+
value: service,
|
|
104
|
+
type: "string"
|
|
105
|
+
};
|
|
106
|
+
if (config.tags) for (const [key, value] of Object.entries(config.tags)) attributes[key] = {
|
|
107
|
+
value,
|
|
108
|
+
type: "string"
|
|
109
|
+
};
|
|
110
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
111
|
+
if (key === "traceId" || key === "spanId") continue;
|
|
112
|
+
if (value === void 0 || value === null) continue;
|
|
113
|
+
const attr = toAttributeValue(value);
|
|
114
|
+
if (attr) attributes[key] = attr;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
timestamp: new Date(timestamp).getTime() / 1e3,
|
|
118
|
+
trace_id: traceId,
|
|
119
|
+
level,
|
|
120
|
+
body,
|
|
121
|
+
severity_number: OTEL_SEVERITY_NUMBER[level] ?? 9,
|
|
122
|
+
attributes
|
|
123
|
+
};
|
|
105
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the Sentry Envelope body for a list of logs.
|
|
127
|
+
*
|
|
128
|
+
* Envelope format (line-delimited):
|
|
129
|
+
* - Line 1: Envelope headers (dsn, sent_at)
|
|
130
|
+
* - Line 2: Item header (type: log, item_count, content_type)
|
|
131
|
+
* - Line 3: Item payload ({"items": [...]})
|
|
132
|
+
*/
|
|
106
133
|
function buildEnvelopeBody(logs, dsn) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
const itemPayload = JSON.stringify({ items: logs });
|
|
117
|
-
return `${envelopeHeader}
|
|
118
|
-
${itemHeader}
|
|
119
|
-
${itemPayload}
|
|
120
|
-
`;
|
|
134
|
+
return `${JSON.stringify({
|
|
135
|
+
dsn,
|
|
136
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
137
|
+
})}\n${JSON.stringify({
|
|
138
|
+
type: "log",
|
|
139
|
+
item_count: logs.length,
|
|
140
|
+
content_type: "application/vnd.sentry.items.log+json"
|
|
141
|
+
})}\n${JSON.stringify({ items: logs })}\n`;
|
|
121
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Create a drain function for sending logs to Sentry.
|
|
145
|
+
*
|
|
146
|
+
* Sends wide events as Sentry Structured Logs, visible in Explore > Logs
|
|
147
|
+
* in the Sentry dashboard.
|
|
148
|
+
*
|
|
149
|
+
* Configuration priority (highest to lowest):
|
|
150
|
+
* 1. Overrides passed to createSentryDrain()
|
|
151
|
+
* 2. runtimeConfig.evlog.sentry
|
|
152
|
+
* 3. runtimeConfig.sentry
|
|
153
|
+
* 4. Environment variables: NUXT_SENTRY_*, SENTRY_*
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* // Zero config - just set NUXT_SENTRY_DSN env var
|
|
158
|
+
* nitroApp.hooks.hook('evlog:drain', createSentryDrain())
|
|
159
|
+
*
|
|
160
|
+
* // With overrides
|
|
161
|
+
* nitroApp.hooks.hook('evlog:drain', createSentryDrain({
|
|
162
|
+
* dsn: 'https://public@o0.ingest.sentry.io/123',
|
|
163
|
+
* }))
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
122
166
|
function createSentryDrain(overrides) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
};
|
|
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
|
+
});
|
|
144
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Send a single event to Sentry as a structured log.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* await sendToSentry(event, {
|
|
186
|
+
* dsn: process.env.SENTRY_DSN!,
|
|
187
|
+
* })
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
145
190
|
async function sendToSentry(event, config) {
|
|
146
|
-
|
|
191
|
+
await sendBatchToSentry([event], config);
|
|
147
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Send a batch of events to Sentry as structured logs via the Envelope endpoint.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* await sendBatchToSentry(events, {
|
|
199
|
+
* dsn: process.env.SENTRY_DSN!,
|
|
200
|
+
* })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
148
203
|
async function sendBatchToSentry(events, config) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}
|
|
204
|
+
if (events.length === 0) return;
|
|
205
|
+
const { url, authHeader } = getSentryEnvelopeUrl(config.dsn);
|
|
206
|
+
const body = buildEnvelopeBody(events.map((event) => toSentryLog(event, config)), config.dsn);
|
|
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
|
+
});
|
|
174
217
|
}
|
|
175
218
|
|
|
219
|
+
//#endregion
|
|
176
220
|
export { createSentryDrain, sendBatchToSentry, sendToSentry, toSentryLog };
|
|
221
|
+
//# sourceMappingURL=sentry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/dist/browser.mjs
ADDED
|
@@ -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"}
|
|
@@ -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"}
|