evlog 1.4.0 → 1.6.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 CHANGED
@@ -431,6 +431,23 @@ Set environment variables:
431
431
  NUXT_OTLP_ENDPOINT=http://localhost:4318
432
432
  ```
433
433
 
434
+ ### Sentry
435
+
436
+ ```typescript
437
+ // server/plugins/evlog-drain.ts
438
+ import { createSentryDrain } from 'evlog/sentry'
439
+
440
+ export default defineNitroPlugin((nitroApp) => {
441
+ nitroApp.hooks.hook('evlog:drain', createSentryDrain())
442
+ })
443
+ ```
444
+
445
+ Set environment variables:
446
+
447
+ ```bash
448
+ NUXT_SENTRY_DSN=https://public@o0.ingest.sentry.io/123
449
+ ```
450
+
434
451
  ### Multiple Destinations
435
452
 
436
453
  Send logs to multiple services:
@@ -1,11 +1,5 @@
1
- function getRuntimeConfig() {
2
- try {
3
- const { useRuntimeConfig } = require("nitropack/runtime");
4
- return useRuntimeConfig();
5
- } catch {
6
- return void 0;
7
- }
8
- }
1
+ import { g as getRuntimeConfig } from '../shared/evlog.Bc35pxiY.mjs';
2
+
9
3
  function createAxiomDrain(overrides) {
10
4
  return async (ctx) => {
11
5
  const runtimeConfig = getRuntimeConfig();
@@ -1,3 +1,5 @@
1
+ import { g as getRuntimeConfig } from '../shared/evlog.Bc35pxiY.mjs';
2
+
1
3
  const SEVERITY_MAP = {
2
4
  debug: 5,
3
5
  // DEBUG
@@ -14,14 +16,6 @@ const SEVERITY_TEXT_MAP = {
14
16
  warn: "WARN",
15
17
  error: "ERROR"
16
18
  };
17
- function getRuntimeConfig() {
18
- try {
19
- const { useRuntimeConfig } = require("nitropack/runtime");
20
- return useRuntimeConfig();
21
- } catch {
22
- return void 0;
23
- }
24
- }
25
19
  function toAttributeValue(value) {
26
20
  if (typeof value === "boolean") {
27
21
  return { boolValue: value };
@@ -0,0 +1,72 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface PostHogConfig {
4
+ /** PostHog project API key */
5
+ apiKey: string;
6
+ /** PostHog host URL. Default: https://us.i.posthog.com */
7
+ host?: string;
8
+ /** PostHog event name. Default: evlog_wide_event */
9
+ eventName?: string;
10
+ /** Override distinct_id (defaults to event.service) */
11
+ distinctId?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** PostHog event structure for the batch API */
16
+ interface PostHogEvent {
17
+ event: string;
18
+ distinct_id: string;
19
+ timestamp: string;
20
+ properties: Record<string, unknown>;
21
+ }
22
+ /**
23
+ * Convert a WideEvent to a PostHog event format.
24
+ */
25
+ declare function toPostHogEvent(event: WideEvent, config: PostHogConfig): PostHogEvent;
26
+ /**
27
+ * Create a drain function for sending logs to PostHog.
28
+ *
29
+ * Configuration priority (highest to lowest):
30
+ * 1. Overrides passed to createPostHogDrain()
31
+ * 2. runtimeConfig.evlog.posthog
32
+ * 3. runtimeConfig.posthog
33
+ * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // Zero config - just set NUXT_POSTHOG_API_KEY env var
38
+ * nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
39
+ *
40
+ * // With overrides
41
+ * nitroApp.hooks.hook('evlog:drain', createPostHogDrain({
42
+ * apiKey: 'phc_...',
43
+ * host: 'https://eu.i.posthog.com',
44
+ * }))
45
+ * ```
46
+ */
47
+ declare function createPostHogDrain(overrides?: Partial<PostHogConfig>): (ctx: DrainContext) => Promise<void>;
48
+ /**
49
+ * Send a single event to PostHog.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendToPostHog(event, {
54
+ * apiKey: process.env.POSTHOG_API_KEY!,
55
+ * })
56
+ * ```
57
+ */
58
+ declare function sendToPostHog(event: WideEvent, config: PostHogConfig): Promise<void>;
59
+ /**
60
+ * Send a batch of events to PostHog.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendBatchToPostHog(events, {
65
+ * apiKey: process.env.POSTHOG_API_KEY!,
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendBatchToPostHog(events: WideEvent[], config: PostHogConfig): Promise<void>;
70
+
71
+ export { createPostHogDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
72
+ export type { PostHogConfig, PostHogEvent };
@@ -0,0 +1,72 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface PostHogConfig {
4
+ /** PostHog project API key */
5
+ apiKey: string;
6
+ /** PostHog host URL. Default: https://us.i.posthog.com */
7
+ host?: string;
8
+ /** PostHog event name. Default: evlog_wide_event */
9
+ eventName?: string;
10
+ /** Override distinct_id (defaults to event.service) */
11
+ distinctId?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** PostHog event structure for the batch API */
16
+ interface PostHogEvent {
17
+ event: string;
18
+ distinct_id: string;
19
+ timestamp: string;
20
+ properties: Record<string, unknown>;
21
+ }
22
+ /**
23
+ * Convert a WideEvent to a PostHog event format.
24
+ */
25
+ declare function toPostHogEvent(event: WideEvent, config: PostHogConfig): PostHogEvent;
26
+ /**
27
+ * Create a drain function for sending logs to PostHog.
28
+ *
29
+ * Configuration priority (highest to lowest):
30
+ * 1. Overrides passed to createPostHogDrain()
31
+ * 2. runtimeConfig.evlog.posthog
32
+ * 3. runtimeConfig.posthog
33
+ * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // Zero config - just set NUXT_POSTHOG_API_KEY env var
38
+ * nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
39
+ *
40
+ * // With overrides
41
+ * nitroApp.hooks.hook('evlog:drain', createPostHogDrain({
42
+ * apiKey: 'phc_...',
43
+ * host: 'https://eu.i.posthog.com',
44
+ * }))
45
+ * ```
46
+ */
47
+ declare function createPostHogDrain(overrides?: Partial<PostHogConfig>): (ctx: DrainContext) => Promise<void>;
48
+ /**
49
+ * Send a single event to PostHog.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendToPostHog(event, {
54
+ * apiKey: process.env.POSTHOG_API_KEY!,
55
+ * })
56
+ * ```
57
+ */
58
+ declare function sendToPostHog(event: WideEvent, config: PostHogConfig): Promise<void>;
59
+ /**
60
+ * Send a batch of events to PostHog.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendBatchToPostHog(events, {
65
+ * apiKey: process.env.POSTHOG_API_KEY!,
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendBatchToPostHog(events: WideEvent[], config: PostHogConfig): Promise<void>;
70
+
71
+ export { createPostHogDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
72
+ export type { PostHogConfig, PostHogEvent };
@@ -0,0 +1,73 @@
1
+ import { g as getRuntimeConfig } from '../shared/evlog.Bc35pxiY.mjs';
2
+
3
+ function toPostHogEvent(event, config) {
4
+ const { timestamp, level, service, ...rest } = event;
5
+ return {
6
+ event: config.eventName ?? "evlog_wide_event",
7
+ distinct_id: config.distinctId ?? service,
8
+ timestamp,
9
+ properties: {
10
+ level,
11
+ service,
12
+ ...rest
13
+ }
14
+ };
15
+ }
16
+ function createPostHogDrain(overrides) {
17
+ return async (ctx) => {
18
+ const runtimeConfig = getRuntimeConfig();
19
+ const evlogPostHog = runtimeConfig?.evlog?.posthog;
20
+ const rootPostHog = runtimeConfig?.posthog;
21
+ const config = {
22
+ apiKey: overrides?.apiKey ?? evlogPostHog?.apiKey ?? rootPostHog?.apiKey ?? process.env.NUXT_POSTHOG_API_KEY ?? process.env.POSTHOG_API_KEY,
23
+ host: overrides?.host ?? evlogPostHog?.host ?? rootPostHog?.host ?? process.env.NUXT_POSTHOG_HOST ?? process.env.POSTHOG_HOST,
24
+ eventName: overrides?.eventName ?? evlogPostHog?.eventName ?? rootPostHog?.eventName,
25
+ distinctId: overrides?.distinctId ?? evlogPostHog?.distinctId ?? rootPostHog?.distinctId,
26
+ timeout: overrides?.timeout ?? evlogPostHog?.timeout ?? rootPostHog?.timeout
27
+ };
28
+ if (!config.apiKey) {
29
+ console.error("[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()");
30
+ return;
31
+ }
32
+ try {
33
+ await sendToPostHog(ctx.event, config);
34
+ } catch (error) {
35
+ console.error("[evlog/posthog] Failed to send event:", error);
36
+ }
37
+ };
38
+ }
39
+ async function sendToPostHog(event, config) {
40
+ await sendBatchToPostHog([event], config);
41
+ }
42
+ async function sendBatchToPostHog(events, config) {
43
+ if (events.length === 0) return;
44
+ const host = (config.host ?? "https://us.i.posthog.com").replace(/\/$/, "");
45
+ const timeout = config.timeout ?? 5e3;
46
+ const url = `${host}/batch/`;
47
+ const batch = events.map((event) => toPostHogEvent(event, config));
48
+ const payload = {
49
+ api_key: config.apiKey,
50
+ batch
51
+ };
52
+ const controller = new AbortController();
53
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
54
+ try {
55
+ const response = await fetch(url, {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json"
59
+ },
60
+ body: JSON.stringify(payload),
61
+ signal: controller.signal
62
+ });
63
+ if (!response.ok) {
64
+ const text = await response.text().catch(() => "Unknown error");
65
+ const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
66
+ throw new Error(`PostHog API error: ${response.status} ${response.statusText} - ${safeText}`);
67
+ }
68
+ } finally {
69
+ clearTimeout(timeoutId);
70
+ }
71
+ }
72
+
73
+ export { createPostHogDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
@@ -0,0 +1,78 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface SentryConfig {
4
+ /** Sentry DSN */
5
+ dsn: string;
6
+ /** Environment override (defaults to event.environment) */
7
+ environment?: string;
8
+ /** Release version override (defaults to event.version) */
9
+ release?: string;
10
+ /** Additional tags to attach as attributes */
11
+ tags?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** Sentry Log attribute value with type annotation */
16
+ interface SentryAttributeValue {
17
+ value: string | number | boolean;
18
+ type: 'string' | 'integer' | 'double' | 'boolean';
19
+ }
20
+ /** Sentry Structured Log payload */
21
+ interface SentryLog {
22
+ timestamp: number;
23
+ trace_id: string;
24
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
25
+ body: string;
26
+ severity_number: number;
27
+ attributes?: Record<string, SentryAttributeValue>;
28
+ }
29
+ declare function toSentryLog(event: WideEvent, config: SentryConfig): SentryLog;
30
+ /**
31
+ * Create a drain function for sending logs to Sentry.
32
+ *
33
+ * Sends wide events as Sentry Structured Logs, visible in Explore > Logs
34
+ * in the Sentry dashboard.
35
+ *
36
+ * Configuration priority (highest to lowest):
37
+ * 1. Overrides passed to createSentryDrain()
38
+ * 2. runtimeConfig.evlog.sentry
39
+ * 3. runtimeConfig.sentry
40
+ * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Zero config - just set NUXT_SENTRY_DSN env var
45
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain())
46
+ *
47
+ * // With overrides
48
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain({
49
+ * dsn: 'https://public@o0.ingest.sentry.io/123',
50
+ * }))
51
+ * ```
52
+ */
53
+ declare function createSentryDrain(overrides?: Partial<SentryConfig>): (ctx: DrainContext) => Promise<void>;
54
+ /**
55
+ * Send a single event to Sentry as a structured log.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * await sendToSentry(event, {
60
+ * dsn: process.env.SENTRY_DSN!,
61
+ * })
62
+ * ```
63
+ */
64
+ declare function sendToSentry(event: WideEvent, config: SentryConfig): Promise<void>;
65
+ /**
66
+ * Send a batch of events to Sentry as structured logs via the Envelope endpoint.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * await sendBatchToSentry(events, {
71
+ * dsn: process.env.SENTRY_DSN!,
72
+ * })
73
+ * ```
74
+ */
75
+ declare function sendBatchToSentry(events: WideEvent[], config: SentryConfig): Promise<void>;
76
+
77
+ export { createSentryDrain, sendBatchToSentry, sendToSentry, toSentryLog };
78
+ export type { SentryAttributeValue, SentryConfig, SentryLog };
@@ -0,0 +1,78 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface SentryConfig {
4
+ /** Sentry DSN */
5
+ dsn: string;
6
+ /** Environment override (defaults to event.environment) */
7
+ environment?: string;
8
+ /** Release version override (defaults to event.version) */
9
+ release?: string;
10
+ /** Additional tags to attach as attributes */
11
+ tags?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** Sentry Log attribute value with type annotation */
16
+ interface SentryAttributeValue {
17
+ value: string | number | boolean;
18
+ type: 'string' | 'integer' | 'double' | 'boolean';
19
+ }
20
+ /** Sentry Structured Log payload */
21
+ interface SentryLog {
22
+ timestamp: number;
23
+ trace_id: string;
24
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
25
+ body: string;
26
+ severity_number: number;
27
+ attributes?: Record<string, SentryAttributeValue>;
28
+ }
29
+ declare function toSentryLog(event: WideEvent, config: SentryConfig): SentryLog;
30
+ /**
31
+ * Create a drain function for sending logs to Sentry.
32
+ *
33
+ * Sends wide events as Sentry Structured Logs, visible in Explore > Logs
34
+ * in the Sentry dashboard.
35
+ *
36
+ * Configuration priority (highest to lowest):
37
+ * 1. Overrides passed to createSentryDrain()
38
+ * 2. runtimeConfig.evlog.sentry
39
+ * 3. runtimeConfig.sentry
40
+ * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Zero config - just set NUXT_SENTRY_DSN env var
45
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain())
46
+ *
47
+ * // With overrides
48
+ * nitroApp.hooks.hook('evlog:drain', createSentryDrain({
49
+ * dsn: 'https://public@o0.ingest.sentry.io/123',
50
+ * }))
51
+ * ```
52
+ */
53
+ declare function createSentryDrain(overrides?: Partial<SentryConfig>): (ctx: DrainContext) => Promise<void>;
54
+ /**
55
+ * Send a single event to Sentry as a structured log.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * await sendToSentry(event, {
60
+ * dsn: process.env.SENTRY_DSN!,
61
+ * })
62
+ * ```
63
+ */
64
+ declare function sendToSentry(event: WideEvent, config: SentryConfig): Promise<void>;
65
+ /**
66
+ * Send a batch of events to Sentry as structured logs via the Envelope endpoint.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * await sendBatchToSentry(events, {
71
+ * dsn: process.env.SENTRY_DSN!,
72
+ * })
73
+ * ```
74
+ */
75
+ declare function sendBatchToSentry(events: WideEvent[], config: SentryConfig): Promise<void>;
76
+
77
+ export { createSentryDrain, sendBatchToSentry, sendToSentry, toSentryLog };
78
+ export type { SentryAttributeValue, SentryConfig, SentryLog };
@@ -0,0 +1,176 @@
1
+ import { g as getRuntimeConfig } from '../shared/evlog.Bc35pxiY.mjs';
2
+
3
+ const SEVERITY_MAP = {
4
+ debug: 5,
5
+ info: 9,
6
+ warn: 13,
7
+ error: 17
8
+ };
9
+ 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
+ };
29
+ }
30
+ 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 };
38
+ }
39
+ 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("");
44
+ }
45
+ 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;
51
+ }
52
+ 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" };
69
+ }
70
+ 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
+ };
105
+ }
106
+ 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
+ `;
121
+ }
122
+ 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
+ };
144
+ }
145
+ async function sendToSentry(event, config) {
146
+ await sendBatchToSentry([event], config);
147
+ }
148
+ 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
+ }
174
+ }
175
+
176
+ export { createSentryDrain, sendBatchToSentry, sendToSentry, toSentryLog };
package/dist/index.mjs CHANGED
@@ -3,4 +3,3 @@ export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from
3
3
  export { useLogger } from './runtime/server/useLogger.mjs';
4
4
  export { parseError } from './runtime/utils/parseError.mjs';
5
5
  import './utils.mjs';
6
- import 'defu';
package/dist/logger.mjs CHANGED
@@ -1,6 +1,21 @@
1
- import { defu } from 'defu';
2
1
  import { isDev, detectEnvironment, formatDuration, matchesPattern, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
3
2
 
3
+ function isPlainObject(val) {
4
+ return val !== null && typeof val === "object" && !Array.isArray(val);
5
+ }
6
+ function deepDefaults(base, defaults) {
7
+ const result = { ...base };
8
+ for (const key in defaults) {
9
+ const baseVal = result[key];
10
+ const defaultVal = defaults[key];
11
+ if (baseVal === void 0 || baseVal === null) {
12
+ result[key] = defaultVal;
13
+ } else if (isPlainObject(baseVal) && isPlainObject(defaultVal)) {
14
+ result[key] = deepDefaults(baseVal, defaultVal);
15
+ }
16
+ }
17
+ return result;
18
+ }
4
19
  let globalEnv = {
5
20
  service: "app",
6
21
  environment: "development"
@@ -154,7 +169,7 @@ function createRequestLogger(options = {}) {
154
169
  let hasError = false;
155
170
  return {
156
171
  set(data) {
157
- context = defu(data, context);
172
+ context = deepDefaults(data, context);
158
173
  },
159
174
  error(error, errorContext) {
160
175
  hasError = true;
@@ -167,7 +182,7 @@ function createRequestLogger(options = {}) {
167
182
  stack: err.stack
168
183
  }
169
184
  };
170
- context = defu(errorData, context);
185
+ context = deepDefaults(errorData, context);
171
186
  },
172
187
  emit(overrides) {
173
188
  const durationMs = Date.now() - startTime;
@@ -2,7 +2,6 @@ import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
2
2
  import { getHeaders } from 'h3';
3
3
  import { initLogger, createRequestLogger } from '../logger.mjs';
4
4
  import { matchesPattern } from '../utils.mjs';
5
- import 'defu';
6
5
 
7
6
  function shouldLog(path, include, exclude) {
8
7
  if (exclude && exclude.length > 0) {
@@ -63,9 +62,9 @@ function callDrainHook(nitroApp, emittedEvent, event) {
63
62
  }).catch((err) => {
64
63
  console.error("[evlog] drain failed:", err);
65
64
  });
66
- const waitUntil = event.context.cloudflare?.context?.waitUntil ?? event.context.waitUntil;
67
- if (typeof waitUntil === "function") {
68
- waitUntil(drainPromise);
65
+ const waitUntilCtx = event.context.cloudflare?.context ?? event.context;
66
+ if (typeof waitUntilCtx?.waitUntil === "function") {
67
+ waitUntilCtx.waitUntil(drainPromise);
69
68
  }
70
69
  }
71
70
  const plugin = defineNitroPlugin((nitroApp) => {
@@ -82,10 +81,15 @@ const plugin = defineNitroPlugin((nitroApp) => {
82
81
  return;
83
82
  }
84
83
  e.context._evlogStartTime = Date.now();
84
+ let requestIdOverride = void 0;
85
+ if (globalThis.navigator?.userAgent === "Cloudflare-Workers") {
86
+ const cfRay = getSafeHeaders(event)?.["cf-ray"];
87
+ if (cfRay) requestIdOverride = cfRay;
88
+ }
85
89
  const log = createRequestLogger({
86
90
  method: e.method,
87
91
  path: e.path,
88
- requestId: e.context.requestId || crypto.randomUUID()
92
+ requestId: requestIdOverride || e.context.requestId || crypto.randomUUID()
89
93
  });
90
94
  const routeService = getServiceForPath(e.path, evlogConfig?.routes);
91
95
  if (routeService) {
@@ -118,6 +118,52 @@ interface ModuleOptions {
118
118
  /** Request timeout in milliseconds. Default: 5000 */
119
119
  timeout?: number;
120
120
  };
121
+ /**
122
+ * PostHog adapter configuration.
123
+ * When configured, use `createPostHogDrain()` from `evlog/posthog` to send logs.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * posthog: {
128
+ * apiKey: process.env.POSTHOG_API_KEY,
129
+ * }
130
+ * ```
131
+ */
132
+ posthog?: {
133
+ /** PostHog project API key */
134
+ apiKey: string;
135
+ /** PostHog host URL. Default: https://us.i.posthog.com */
136
+ host?: string;
137
+ /** PostHog event name. Default: evlog_wide_event */
138
+ eventName?: string;
139
+ /** Override distinct_id (defaults to event.service) */
140
+ distinctId?: string;
141
+ /** Request timeout in milliseconds. Default: 5000 */
142
+ timeout?: number;
143
+ };
144
+ /**
145
+ * Sentry adapter configuration.
146
+ * When configured, use `createSentryDrain()` from `evlog/sentry` to send logs.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * sentry: {
151
+ * dsn: process.env.SENTRY_DSN,
152
+ * }
153
+ * ```
154
+ */
155
+ sentry?: {
156
+ /** Sentry DSN */
157
+ dsn: string;
158
+ /** Environment override (defaults to event.environment) */
159
+ environment?: string;
160
+ /** Release version override (defaults to event.version) */
161
+ release?: string;
162
+ /** Additional tags to attach as attributes */
163
+ tags?: Record<string, string>;
164
+ /** Request timeout in milliseconds. Default: 5000 */
165
+ timeout?: number;
166
+ };
121
167
  }
122
168
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
123
169
 
@@ -118,6 +118,52 @@ interface ModuleOptions {
118
118
  /** Request timeout in milliseconds. Default: 5000 */
119
119
  timeout?: number;
120
120
  };
121
+ /**
122
+ * PostHog adapter configuration.
123
+ * When configured, use `createPostHogDrain()` from `evlog/posthog` to send logs.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * posthog: {
128
+ * apiKey: process.env.POSTHOG_API_KEY,
129
+ * }
130
+ * ```
131
+ */
132
+ posthog?: {
133
+ /** PostHog project API key */
134
+ apiKey: string;
135
+ /** PostHog host URL. Default: https://us.i.posthog.com */
136
+ host?: string;
137
+ /** PostHog event name. Default: evlog_wide_event */
138
+ eventName?: string;
139
+ /** Override distinct_id (defaults to event.service) */
140
+ distinctId?: string;
141
+ /** Request timeout in milliseconds. Default: 5000 */
142
+ timeout?: number;
143
+ };
144
+ /**
145
+ * Sentry adapter configuration.
146
+ * When configured, use `createSentryDrain()` from `evlog/sentry` to send logs.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * sentry: {
151
+ * dsn: process.env.SENTRY_DSN,
152
+ * }
153
+ * ```
154
+ */
155
+ sentry?: {
156
+ /** Sentry DSN */
157
+ dsn: string;
158
+ /** Environment override (defaults to event.environment) */
159
+ environment?: string;
160
+ /** Release version override (defaults to event.version) */
161
+ release?: string;
162
+ /** Additional tags to attach as attributes */
163
+ tags?: Record<string, string>;
164
+ /** Request timeout in milliseconds. Default: 5000 */
165
+ timeout?: number;
166
+ };
121
167
  }
122
168
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
123
169
 
@@ -1,7 +1,6 @@
1
1
  import { defineEventHandler, readBody, setResponseStatus, getHeader, getRequestHost, createError } from 'h3';
2
2
  import { useNitroApp } from 'nitropack/runtime';
3
3
  import { getEnvironment } from '../../../../logger.mjs';
4
- import 'defu';
5
4
  import '../../../../utils.mjs';
6
5
 
7
6
  const VALID_LEVELS = ["info", "error", "warn", "debug"];
@@ -0,0 +1,10 @@
1
+ function getRuntimeConfig() {
2
+ try {
3
+ const { useRuntimeConfig } = require("nitropack/runtime");
4
+ return useRuntimeConfig();
5
+ } catch {
6
+ return void 0;
7
+ }
8
+ }
9
+
10
+ export { getRuntimeConfig as g };
package/dist/workers.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createRequestLogger, initLogger } from './logger.mjs';
2
- import 'defu';
3
2
  import './utils.mjs';
4
3
 
5
4
  function isRecord(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evlog",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Wide event logging library with structured error handling. Inspired by LoggingSucks.",
5
5
  "author": "HugoRCD <contact@hrcd.fr>",
6
6
  "homepage": "https://evlog.dev",
@@ -46,6 +46,14 @@
46
46
  "./otlp": {
47
47
  "types": "./dist/adapters/otlp.d.mts",
48
48
  "import": "./dist/adapters/otlp.mjs"
49
+ },
50
+ "./posthog": {
51
+ "types": "./dist/adapters/posthog.d.mts",
52
+ "import": "./dist/adapters/posthog.mjs"
53
+ },
54
+ "./sentry": {
55
+ "types": "./dist/adapters/sentry.d.mts",
56
+ "import": "./dist/adapters/sentry.mjs"
49
57
  }
50
58
  },
51
59
  "main": "./dist/index.mjs",
@@ -69,6 +77,12 @@
69
77
  ],
70
78
  "otlp": [
71
79
  "./dist/adapters/otlp.d.mts"
80
+ ],
81
+ "posthog": [
82
+ "./dist/adapters/posthog.d.mts"
83
+ ],
84
+ "sentry": [
85
+ "./dist/adapters/sentry.d.mts"
72
86
  ]
73
87
  }
74
88
  },
@@ -87,27 +101,27 @@
87
101
  "test:coverage": "vitest run --coverage",
88
102
  "typecheck": "echo 'Typecheck handled by build'"
89
103
  },
90
- "dependencies": {
91
- "@nuxt/kit": "^4.3.0",
92
- "defu": "^6.1.4"
93
- },
94
104
  "devDependencies": {
95
105
  "@nuxt/devtools": "^3.1.1",
96
- "@nuxt/schema": "^4.3.0",
106
+ "@nuxt/schema": "^4.3.1",
97
107
  "@nuxt/test-utils": "^3.23.0",
98
108
  "changelogen": "^0.6.2",
99
109
  "h3": "^1.15.5",
100
110
  "nitropack": "^2.13.1",
101
- "nuxt": "^4.3.0",
111
+ "nuxt": "^4.3.1",
102
112
  "typescript": "^5.9.3",
103
113
  "unbuild": "^3.6.1"
104
114
  },
105
115
  "peerDependencies": {
116
+ "@nuxt/kit": "^4.3.1",
106
117
  "h3": "^1.15.5",
107
118
  "nitropack": "^2.13.1",
108
119
  "ofetch": "^1.5.1"
109
120
  },
110
121
  "peerDependenciesMeta": {
122
+ "@nuxt/kit": {
123
+ "optional": true
124
+ },
111
125
  "h3": {
112
126
  "optional": true
113
127
  },