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