@od-oneapp/analytics 2026.2.1701 → 2026.2.2301-canary
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/CHANGELOG.md +1 -1
- package/{ai-YMnynb-t.mjs → ai-Co8hBoEj.mjs} +2 -2
- package/{ai-YMnynb-t.mjs.map → ai-Co8hBoEj.mjs.map} +1 -1
- package/{client-D339NFJS.mjs → client-B8gfgflr.mjs} +1 -1
- package/{client-D339NFJS.mjs.map → client-B8gfgflr.mjs.map} +1 -1
- package/{client-CeOLjbac.mjs → client-C35AzV71.mjs} +21 -6
- package/client-C35AzV71.mjs.map +1 -0
- package/{client-CcFTauAh.mjs → client-DDehaDSz.mjs} +1 -1
- package/{client-CcFTauAh.mjs.map → client-DDehaDSz.mjs.map} +1 -1
- package/{client-CTzJVFU5.mjs → client-DK8twEdp.mjs} +2 -2
- package/{client-CTzJVFU5.mjs.map → client-DK8twEdp.mjs.map} +1 -1
- package/client-next.d.mts +7 -7
- package/client-next.d.mts.map +1 -1
- package/client-next.mjs +1615 -31
- package/client-next.mjs.map +1 -1
- package/client.d.mts +9 -9
- package/client.mjs +8 -116
- package/client.mjs.map +1 -1
- package/{config-P6P5adJg.mjs → config-6Mwe7b2O.mjs} +1 -1
- package/{config-P6P5adJg.mjs.map → config-6Mwe7b2O.mjs.map} +1 -1
- package/{config-DPS6bSYo.d.mts → config-Ciu7O6n1.d.mts} +2 -2
- package/{config-DPS6bSYo.d.mts.map → config-Ciu7O6n1.d.mts.map} +1 -1
- package/{console-8bND3mMU.mjs → console-BpU88FNF.mjs} +2 -2
- package/console-BpU88FNF.mjs.map +1 -0
- package/core-DBLE5iTF.mjs +95 -0
- package/core-DBLE5iTF.mjs.map +1 -0
- package/{ecommerce-Cgu4wlux.mjs → ecommerce-DGG1FbiH.mjs} +2 -2
- package/{ecommerce-Cgu4wlux.mjs.map → ecommerce-DGG1FbiH.mjs.map} +1 -1
- package/{emitters-DldkVSPp.d.mts → emitters-BDSsleo_.d.mts} +2 -2
- package/{emitters-DldkVSPp.d.mts.map → emitters-BDSsleo_.d.mts.map} +1 -1
- package/{emitters-6-nKo8i-.mjs → emitters-BvEelkxS.mjs} +1 -1
- package/{emitters-6-nKo8i-.mjs.map → emitters-BvEelkxS.mjs.map} +1 -1
- package/{index-jPzXRn52.d.mts → index-BWhDEs8u.d.mts} +3 -3
- package/{index-jPzXRn52.d.mts.map → index-BWhDEs8u.d.mts.map} +1 -1
- package/{index-BkIWe--N.d.mts → index-Cp-N57Zb.d.mts} +2 -2
- package/{index-BkIWe--N.d.mts.map → index-Cp-N57Zb.d.mts.map} +1 -1
- package/{index-BfNWgfa5.d.mts → index-DTvdqV7H.d.mts} +14 -2
- package/{index-BfNWgfa5.d.mts.map → index-DTvdqV7H.d.mts.map} +1 -1
- package/{manager-DvRRjza6.d.mts → manager-OJpSKwqb.d.mts} +3 -2
- package/manager-OJpSKwqb.d.mts.map +1 -0
- package/module-DVAU7zKb.mjs +5850 -0
- package/module-DVAU7zKb.mjs.map +1 -0
- package/package.json +42 -37
- package/{posthog-bootstrap-DWxFrxlt.d.mts → posthog-bootstrap-Bu1BfhVv.d.mts} +3 -3
- package/{posthog-bootstrap-DWxFrxlt.d.mts.map → posthog-bootstrap-Bu1BfhVv.d.mts.map} +1 -1
- package/{posthog-bootstrap-CYfIy_WS.mjs → posthog-bootstrap-DkPdn-hA.mjs} +81 -46
- package/posthog-bootstrap-DkPdn-hA.mjs.map +1 -0
- package/providers-http-client.d.mts +1 -1
- package/providers-http-client.d.mts.map +1 -1
- package/providers-http-client.mjs +35 -7
- package/providers-http-client.mjs.map +1 -1
- package/providers-http-server.d.mts +1 -1
- package/providers-http-server.d.mts.map +1 -1
- package/providers-http-server.mjs +19 -3
- package/providers-http-server.mjs.map +1 -1
- package/providers-http.d.mts +9 -1
- package/providers-http.d.mts.map +1 -1
- package/server-edge.d.mts +3 -3
- package/server-edge.mjs +4 -4
- package/server-edge.mjs.map +1 -1
- package/server-next.d.mts +9 -9
- package/server-next.mjs +5 -5
- package/server.d.mts +9 -9
- package/server.mjs +5 -5
- package/{service-Duqnlppl.mjs → service-NuHnv30x.mjs} +51 -119
- package/service-NuHnv30x.mjs.map +1 -0
- package/shared.d.mts +4 -4
- package/shared.mjs +3 -3
- package/{types-CBvxUEaF.d.mts → types-DEcTnnFe.d.mts} +1 -1
- package/{types-CBvxUEaF.d.mts.map → types-DEcTnnFe.d.mts.map} +1 -1
- package/{types-BxBnNQ0V.d.mts → types-cMMfHIpi.d.mts} +1 -1
- package/{types-BxBnNQ0V.d.mts.map → types-cMMfHIpi.d.mts.map} +1 -1
- package/types.d.mts +3 -3
- package/{vercel-types-lwakUfoI.d.mts → vercel-types-oM7Sn385.d.mts} +1 -1
- package/{vercel-types-lwakUfoI.d.mts.map → vercel-types-oM7Sn385.d.mts.map} +1 -1
- package/client-CeOLjbac.mjs.map +0 -1
- package/console-8bND3mMU.mjs.map +0 -1
- package/manager-DvRRjza6.d.mts.map +0 -1
- package/posthog-bootstrap-CYfIy_WS.mjs.map +0 -1
- package/service-Duqnlppl.mjs.map +0 -1
package/client-next.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { a as
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
3
|
+
import { i as logWarn, n as logError, t as logDebug } from "./core-DBLE5iTF.mjs";
|
|
4
|
+
import { a as ContextBuilder, c as createAnonymousSession, d as isGroupPayload, f as isIdentifyPayload, g as withUTM, h as withMetadata, i as getDistinctIdFromCookies, l as createUserSession, m as isTrackPayload, n as createMinimalBootstrapData, o as EventBatch, p as isPagePayload, r as generateDistinctId, s as PayloadBuilder, t as createBootstrapData, u as isAliasPayload, v as createAnalyticsManager } from "./posthog-bootstrap-DkPdn-hA.mjs";
|
|
5
|
+
import { a as screen, i as page, n as group, o as track, r as identify, t as alias } from "./emitters-BvEelkxS.mjs";
|
|
6
|
+
import { t as ecommerce_exports } from "./ecommerce-DGG1FbiH.mjs";
|
|
7
|
+
import { a as createEmitterProcessor, i as validateConfig, n as createConfigBuilder, o as processEmitterPayload, r as getAnalyticsConfig, s as trackEcommerceEvent, t as PROVIDER_REQUIREMENTS } from "./config-6Mwe7b2O.mjs";
|
|
7
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
9
|
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
9
|
-
import {
|
|
10
|
+
import { createEnv } from "@t3-oss/env-core";
|
|
11
|
+
import { z } from "zod/v4";
|
|
10
12
|
import { jsx } from "react/jsx-runtime";
|
|
11
13
|
|
|
12
14
|
//#region src/client/next/manager.ts
|
|
@@ -25,7 +27,6 @@ import { jsx } from "react/jsx-runtime";
|
|
|
25
27
|
*
|
|
26
28
|
* @module @od-oneapp/analytics/client/next/manager
|
|
27
29
|
*/
|
|
28
|
-
const logWarn = (_message, _context) => {};
|
|
29
30
|
/**
|
|
30
31
|
* Create a Next.js client analytics instance with runtime provider loading
|
|
31
32
|
* Only imports providers that are actually configured to avoid webpack bundling unused dependencies
|
|
@@ -45,7 +46,7 @@ async function createNextJSClientAnalytics(config) {
|
|
|
45
46
|
case "console": {
|
|
46
47
|
const module = await import(
|
|
47
48
|
/* webpackChunkName: "analytics-console" */
|
|
48
|
-
"./client-
|
|
49
|
+
"./client-DK8twEdp.mjs"
|
|
49
50
|
).then((n) => n.t);
|
|
50
51
|
CLIENT_PROVIDERS.console = (config) => new module.ConsoleProvider(config);
|
|
51
52
|
break;
|
|
@@ -53,7 +54,7 @@ async function createNextJSClientAnalytics(config) {
|
|
|
53
54
|
case "posthog": {
|
|
54
55
|
const module = await import(
|
|
55
56
|
/* webpackChunkName: "analytics-posthog" */
|
|
56
|
-
"./client-
|
|
57
|
+
"./client-C35AzV71.mjs"
|
|
57
58
|
);
|
|
58
59
|
CLIENT_PROVIDERS.posthog = (config) => new module.PostHogClientProvider(config);
|
|
59
60
|
break;
|
|
@@ -61,15 +62,23 @@ async function createNextJSClientAnalytics(config) {
|
|
|
61
62
|
case "segment": {
|
|
62
63
|
const module = await import(
|
|
63
64
|
/* webpackChunkName: "analytics-segment" */
|
|
64
|
-
"./client-
|
|
65
|
+
"./client-B8gfgflr.mjs"
|
|
65
66
|
);
|
|
66
67
|
CLIENT_PROVIDERS.segment = (config) => new module.SegmentClientProvider(config);
|
|
67
68
|
break;
|
|
68
69
|
}
|
|
70
|
+
case "http": {
|
|
71
|
+
const module = await import(
|
|
72
|
+
/* webpackChunkName: "analytics-http" */
|
|
73
|
+
"./providers-http-client.mjs"
|
|
74
|
+
);
|
|
75
|
+
CLIENT_PROVIDERS.http = (config) => new module.HttpClientProvider(config);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
69
78
|
case "vercel": {
|
|
70
79
|
const module = await import(
|
|
71
80
|
/* webpackChunkName: "analytics-vercel" */
|
|
72
|
-
"./client-
|
|
81
|
+
"./client-DDehaDSz.mjs"
|
|
73
82
|
);
|
|
74
83
|
CLIENT_PROVIDERS.vercel = (config) => new module.VercelClientProvider(config);
|
|
75
84
|
break;
|
|
@@ -99,22 +108,27 @@ async function createNextJSClientAnalyticsUninitialized(config) {
|
|
|
99
108
|
const configuredProviders = Object.keys(config.providers);
|
|
100
109
|
for (const providerName of configuredProviders) switch (providerName) {
|
|
101
110
|
case "console": {
|
|
102
|
-
const { ConsoleProvider } = await import("./client-
|
|
111
|
+
const { ConsoleProvider } = await import("./client-DK8twEdp.mjs").then((n) => n.t);
|
|
103
112
|
CLIENT_PROVIDERS.console = (config) => new ConsoleProvider(config);
|
|
104
113
|
break;
|
|
105
114
|
}
|
|
106
115
|
case "posthog": {
|
|
107
|
-
const { PostHogClientProvider } = await import("./client-
|
|
116
|
+
const { PostHogClientProvider } = await import("./client-C35AzV71.mjs");
|
|
108
117
|
CLIENT_PROVIDERS.posthog = (config) => new PostHogClientProvider(config);
|
|
109
118
|
break;
|
|
110
119
|
}
|
|
111
120
|
case "segment": {
|
|
112
|
-
const { SegmentClientProvider } = await import("./client-
|
|
121
|
+
const { SegmentClientProvider } = await import("./client-B8gfgflr.mjs");
|
|
113
122
|
CLIENT_PROVIDERS.segment = (config) => new SegmentClientProvider(config);
|
|
114
123
|
break;
|
|
115
124
|
}
|
|
125
|
+
case "http": {
|
|
126
|
+
const { HttpClientProvider } = await import("./providers-http-client.mjs");
|
|
127
|
+
CLIENT_PROVIDERS.http = (config) => new HttpClientProvider(config);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
116
130
|
case "vercel": {
|
|
117
|
-
const { VercelClientProvider } = await import("./client-
|
|
131
|
+
const { VercelClientProvider } = await import("./client-DDehaDSz.mjs");
|
|
118
132
|
CLIENT_PROVIDERS.vercel = (config) => new VercelClientProvider(config);
|
|
119
133
|
break;
|
|
120
134
|
}
|
|
@@ -125,6 +139,16 @@ async function createNextJSClientAnalyticsUninitialized(config) {
|
|
|
125
139
|
return createAnalyticsManager(config, CLIENT_PROVIDERS);
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/client/next/state.ts
|
|
144
|
+
let globalAnalytics = null;
|
|
145
|
+
function getGlobalAnalytics() {
|
|
146
|
+
return globalAnalytics;
|
|
147
|
+
}
|
|
148
|
+
function setGlobalAnalytics(analytics) {
|
|
149
|
+
globalAnalytics = analytics;
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
//#endregion
|
|
129
153
|
//#region src/client/next/hooks.ts
|
|
130
154
|
/**
|
|
@@ -146,7 +170,6 @@ async function createNextJSClientAnalyticsUninitialized(config) {
|
|
|
146
170
|
*
|
|
147
171
|
* @module @od-oneapp/analytics/client/next/hooks
|
|
148
172
|
*/
|
|
149
|
-
let globalAnalytics$1 = null;
|
|
150
173
|
/**
|
|
151
174
|
* Hook to get or create analytics instance.
|
|
152
175
|
*
|
|
@@ -170,19 +193,25 @@ let globalAnalytics$1 = null;
|
|
|
170
193
|
function useAnalytics(config) {
|
|
171
194
|
const [analytics, setAnalytics] = useState(null);
|
|
172
195
|
useEffect(() => {
|
|
173
|
-
|
|
196
|
+
const currentAnalytics = getGlobalAnalytics();
|
|
197
|
+
if (!currentAnalytics && config) {
|
|
174
198
|
const initAnalytics = async () => {
|
|
175
199
|
try {
|
|
176
200
|
const instance = await createNextJSClientAnalytics(config);
|
|
177
|
-
|
|
201
|
+
setGlobalAnalytics(instance);
|
|
178
202
|
setAnalytics(instance);
|
|
179
|
-
} catch {
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logError("Failed to initialize Next.js analytics client", {
|
|
205
|
+
error: error instanceof Error ? error.message : String(error),
|
|
206
|
+
configProviders: Object.keys(config.providers)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
180
209
|
};
|
|
181
210
|
initAnalytics();
|
|
182
|
-
} else if (
|
|
183
|
-
setAnalytics(
|
|
211
|
+
} else if (currentAnalytics && analytics !== currentAnalytics) setTimeout(() => {
|
|
212
|
+
setAnalytics(currentAnalytics);
|
|
184
213
|
}, 0);
|
|
185
|
-
}, [config]);
|
|
214
|
+
}, [analytics, config]);
|
|
186
215
|
return analytics;
|
|
187
216
|
}
|
|
188
217
|
/**
|
|
@@ -209,7 +238,8 @@ function usePageTracking(options) {
|
|
|
209
238
|
const params = useParams();
|
|
210
239
|
const tracked = useRef("");
|
|
211
240
|
useEffect(() => {
|
|
212
|
-
|
|
241
|
+
const analyticsInstance = getGlobalAnalytics();
|
|
242
|
+
if (options?.skip || !analyticsInstance) return;
|
|
213
243
|
const searchString = options?.trackSearch ? searchParams.toString() : "";
|
|
214
244
|
const pageKey = `${pathname}${searchString}`;
|
|
215
245
|
if (tracked.current === pageKey) return;
|
|
@@ -226,7 +256,7 @@ function usePageTracking(options) {
|
|
|
226
256
|
properties.search_params = Object.fromEntries(searchParams.entries());
|
|
227
257
|
}
|
|
228
258
|
if (options?.trackParams && params) properties.route_params = params;
|
|
229
|
-
|
|
259
|
+
analyticsInstance.page(pathname, properties);
|
|
230
260
|
}, [
|
|
231
261
|
pathname,
|
|
232
262
|
searchParams,
|
|
@@ -254,8 +284,9 @@ function usePageTracking(options) {
|
|
|
254
284
|
*/
|
|
255
285
|
function useTrackEvent() {
|
|
256
286
|
return useCallback((event, properties, options) => {
|
|
257
|
-
|
|
258
|
-
|
|
287
|
+
const analyticsInstance = getGlobalAnalytics();
|
|
288
|
+
if (!analyticsInstance) return;
|
|
289
|
+
analyticsInstance.track(event, properties, options);
|
|
259
290
|
}, []);
|
|
260
291
|
}
|
|
261
292
|
/**
|
|
@@ -275,8 +306,9 @@ function useTrackEvent() {
|
|
|
275
306
|
*/
|
|
276
307
|
function useIdentifyUser() {
|
|
277
308
|
return useCallback((userId, traits, options) => {
|
|
278
|
-
|
|
279
|
-
|
|
309
|
+
const analyticsInstance = getGlobalAnalytics();
|
|
310
|
+
if (!analyticsInstance) return;
|
|
311
|
+
analyticsInstance.identify(userId, traits, options);
|
|
280
312
|
}, []);
|
|
281
313
|
}
|
|
282
314
|
/**
|
|
@@ -294,7 +326,1560 @@ function useIdentifyUser() {
|
|
|
294
326
|
* ```
|
|
295
327
|
*/
|
|
296
328
|
function resetAnalytics() {
|
|
297
|
-
|
|
329
|
+
setGlobalAnalytics(null);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region ../observability/env.ts
|
|
334
|
+
/**
|
|
335
|
+
* @fileoverview Core environment configuration for the observability package
|
|
336
|
+
* Core environment configuration for the observability package
|
|
337
|
+
* Provider-specific configurations are handled by their respective plugins
|
|
338
|
+
*/
|
|
339
|
+
const env$2 = createEnv({
|
|
340
|
+
server: {},
|
|
341
|
+
clientPrefix: "NEXT_PUBLIC_",
|
|
342
|
+
client: {
|
|
343
|
+
NEXT_PUBLIC_NODE_ENV: z.enum([
|
|
344
|
+
"development",
|
|
345
|
+
"test",
|
|
346
|
+
"production"
|
|
347
|
+
]).default("development"),
|
|
348
|
+
NEXT_PUBLIC_OBSERVABILITY_DEBUG: z.string().optional().transform((val) => val === "true").default(false),
|
|
349
|
+
NEXT_PUBLIC_OBSERVABILITY_CONSOLE_ENABLED: z.string().optional().transform((val) => val === "true" ? true : val === "false" ? false : void 0)
|
|
350
|
+
},
|
|
351
|
+
runtimeEnv: process.env,
|
|
352
|
+
emptyStringAsUndefined: true,
|
|
353
|
+
onInvalidAccess: (variable) => {
|
|
354
|
+
throw new Error(`❌ Attempted to access a server-side environment variable on the client: ${variable}`);
|
|
355
|
+
},
|
|
356
|
+
onValidationError: (error) => {
|
|
357
|
+
logWarn("Observability package environment validation failed", { error });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region ../observability/src/core/manager.ts
|
|
363
|
+
/**
|
|
364
|
+
* @fileoverview ObservabilityManager - Core orchestrator for multiple observability providers
|
|
365
|
+
* ObservabilityManager - Core orchestrator for multiple observability providers
|
|
366
|
+
*/
|
|
367
|
+
/**
|
|
368
|
+
* Manager that orchestrates multiple observability plugins
|
|
369
|
+
* Broadcasts all method calls to enabled plugins
|
|
370
|
+
*/
|
|
371
|
+
var ObservabilityManager = class {
|
|
372
|
+
plugins = /* @__PURE__ */ new Map();
|
|
373
|
+
initialized = false;
|
|
374
|
+
initializationPromise = null;
|
|
375
|
+
lifecycle = {};
|
|
376
|
+
initializationError = null;
|
|
377
|
+
/**
|
|
378
|
+
* Create a new ObservabilityManager instance.
|
|
379
|
+
*
|
|
380
|
+
* @param lifecycle - Optional lifecycle callbacks for plugin events
|
|
381
|
+
*/
|
|
382
|
+
constructor(lifecycle) {
|
|
383
|
+
if (lifecycle) this.lifecycle = lifecycle;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Add a plugin to the manager
|
|
387
|
+
*/
|
|
388
|
+
addPlugin(plugin) {
|
|
389
|
+
this.plugins.set(plugin.name, plugin);
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get a specific plugin by name.
|
|
394
|
+
*
|
|
395
|
+
* @param name - Name of the plugin to retrieve
|
|
396
|
+
* @returns Plugin instance if found, undefined otherwise
|
|
397
|
+
*/
|
|
398
|
+
getPlugin(name) {
|
|
399
|
+
return this.plugins.get(name);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get all registered plugins.
|
|
403
|
+
*
|
|
404
|
+
* @returns Array of all registered plugins
|
|
405
|
+
*/
|
|
406
|
+
getPlugins() {
|
|
407
|
+
return Array.from(this.plugins.values());
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* List all registered plugins (alias for getPlugins).
|
|
411
|
+
*
|
|
412
|
+
* @returns Array of all registered plugins
|
|
413
|
+
*/
|
|
414
|
+
listPlugins() {
|
|
415
|
+
return this.getPlugins();
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Initialize all plugins
|
|
419
|
+
* Note: Failed initialization is retryable - subsequent calls will re-attempt initialization.
|
|
420
|
+
* This allows recovery from transient failures (network issues, temporary misconfigurations).
|
|
421
|
+
*/
|
|
422
|
+
async initialize() {
|
|
423
|
+
if (this.initializationPromise) return this.initializationPromise;
|
|
424
|
+
if (this.initialized) return;
|
|
425
|
+
if (this.initializationError) {
|
|
426
|
+
logWarn("ObservabilityManager: Retrying failed initialization");
|
|
427
|
+
this.initializationError = null;
|
|
428
|
+
}
|
|
429
|
+
this.initializationPromise = this.doInitialize();
|
|
430
|
+
try {
|
|
431
|
+
await this.initializationPromise;
|
|
432
|
+
} finally {
|
|
433
|
+
this.initializationPromise = null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async doInitialize() {
|
|
437
|
+
const initPromises = Array.from(this.plugins.values()).filter((plugin) => plugin.enabled && plugin.initialize).map(async (plugin) => {
|
|
438
|
+
try {
|
|
439
|
+
if (plugin.initialize) await plugin.initialize();
|
|
440
|
+
this.lifecycle.onInitialized?.(plugin);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
logError(`Failed to initialize plugin ${plugin.name}`, {
|
|
443
|
+
error,
|
|
444
|
+
pluginName: plugin.name
|
|
445
|
+
});
|
|
446
|
+
this.lifecycle.onError?.(error, plugin);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
await Promise.all(initPromises);
|
|
452
|
+
this.initialized = true;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.initializationError = error instanceof Error ? error : new Error(String(error));
|
|
455
|
+
throw this.initializationError;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Check if initialization had an error
|
|
460
|
+
* @returns `true` if initialization failed, `false` otherwise
|
|
461
|
+
*/
|
|
462
|
+
hasInitializationError() {
|
|
463
|
+
return this.initializationError !== null;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get the initialization error if one occurred
|
|
467
|
+
* @returns The initialization error or `null` if no error
|
|
468
|
+
*/
|
|
469
|
+
getInitializationError() {
|
|
470
|
+
return this.initializationError;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Shutdown all plugins
|
|
474
|
+
*/
|
|
475
|
+
async shutdown() {
|
|
476
|
+
const shutdownPromises = Array.from(this.plugins.values()).filter((plugin) => plugin.shutdown).map(async (plugin) => {
|
|
477
|
+
try {
|
|
478
|
+
if (plugin.shutdown) await plugin.shutdown();
|
|
479
|
+
this.lifecycle.onShutdown?.(plugin);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
logError(`Failed to shutdown plugin ${plugin.name}`, {
|
|
482
|
+
error,
|
|
483
|
+
pluginName: plugin.name
|
|
484
|
+
});
|
|
485
|
+
this.lifecycle.onError?.(error, plugin);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
await Promise.allSettled(shutdownPromises);
|
|
489
|
+
this.initialized = false;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Broadcast exception to all enabled plugins.
|
|
493
|
+
*
|
|
494
|
+
* @param error - Error object or unknown error value
|
|
495
|
+
* @param context - Optional additional context data
|
|
496
|
+
*/
|
|
497
|
+
captureException(error, context) {
|
|
498
|
+
this.broadcast((plugin) => plugin.captureException(error, context));
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Broadcast message to all enabled plugins.
|
|
502
|
+
*
|
|
503
|
+
* @param message - Message to log
|
|
504
|
+
* @param level - Log level (default: 'info')
|
|
505
|
+
* @param context - Optional additional context data
|
|
506
|
+
*/
|
|
507
|
+
captureMessage(message, level = "info", context) {
|
|
508
|
+
this.broadcast((plugin) => plugin.captureMessage(message, level, context));
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Validate and sanitize user data to prevent injection and DoS attacks
|
|
512
|
+
* @param user - User data to validate
|
|
513
|
+
* @returns Validated user data with length limits and format validation applied
|
|
514
|
+
*/
|
|
515
|
+
validateUser(user) {
|
|
516
|
+
if (!user) return null;
|
|
517
|
+
const id = String(user.id).trim().slice(0, 255);
|
|
518
|
+
if (!id) {
|
|
519
|
+
logWarn("ObservabilityManager: User ID is empty, ignoring user data");
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const validated = { id };
|
|
523
|
+
if ("email" in user && user.email) {
|
|
524
|
+
const email = String(user.email).trim().slice(0, 255);
|
|
525
|
+
if (email && /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email)) validated.email = email;
|
|
526
|
+
}
|
|
527
|
+
if ("username" in user && user.username) {
|
|
528
|
+
const username = String(user.username).trim().slice(0, 255);
|
|
529
|
+
if (username) validated.username = username;
|
|
530
|
+
}
|
|
531
|
+
if ("ip_address" in user && user.ip_address) {
|
|
532
|
+
const ip = String(user.ip_address).trim();
|
|
533
|
+
const isValidIPv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip);
|
|
534
|
+
const looksLikeIPv6 = /^[0-9a-fA-F:]+$/.test(ip) && ip.includes(":") && ip.split(":").length <= 8;
|
|
535
|
+
if (isValidIPv4 || looksLikeIPv6) validated.ip_address = ip.slice(0, 45);
|
|
536
|
+
}
|
|
537
|
+
return validated;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Set user on all enabled plugins
|
|
541
|
+
* User data is validated and sanitized before being set
|
|
542
|
+
* @param user - User data to set (will be validated)
|
|
543
|
+
*/
|
|
544
|
+
setUser(user) {
|
|
545
|
+
const validatedUser = this.validateUser(user);
|
|
546
|
+
this.broadcast((plugin) => plugin.setUser(validatedUser));
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Add breadcrumb to all enabled plugins.
|
|
550
|
+
*
|
|
551
|
+
* @param breadcrumb - Breadcrumb data to add
|
|
552
|
+
*/
|
|
553
|
+
addBreadcrumb(breadcrumb) {
|
|
554
|
+
this.broadcast((plugin) => plugin.addBreadcrumb(breadcrumb));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Execute callback within scope for all enabled plugins.
|
|
558
|
+
*
|
|
559
|
+
* @param callback - Callback that receives the scope
|
|
560
|
+
*/
|
|
561
|
+
withScope(callback) {
|
|
562
|
+
this.broadcast((plugin) => plugin.withScope(callback));
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Flush all plugins that support it.
|
|
566
|
+
*
|
|
567
|
+
* Waits for all enabled plugins with flush capability to send pending events.
|
|
568
|
+
*
|
|
569
|
+
* @param timeout - Maximum time to wait in milliseconds
|
|
570
|
+
* @returns Promise resolving to true if all plugins flushed successfully
|
|
571
|
+
*/
|
|
572
|
+
async flush(timeout) {
|
|
573
|
+
const flushPromises = Array.from(this.plugins.values()).filter((plugin) => {
|
|
574
|
+
return plugin.enabled && "flush" in plugin && typeof plugin.flush === "function";
|
|
575
|
+
}).map((plugin) => plugin.flush(timeout));
|
|
576
|
+
if (flushPromises.length === 0) return true;
|
|
577
|
+
return (await Promise.allSettled(flushPromises)).every((result) => result.status === "fulfilled" && result.value === true);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Helper to broadcast a method call to all enabled plugins
|
|
581
|
+
* Uses nested try-catch to ensure errors in error handling don't prevent other plugins from executing
|
|
582
|
+
*/
|
|
583
|
+
broadcast(fn) {
|
|
584
|
+
this.plugins.forEach((plugin) => {
|
|
585
|
+
if (plugin.enabled) try {
|
|
586
|
+
fn(plugin);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
try {
|
|
589
|
+
logError(`Plugin ${plugin.name} error`, {
|
|
590
|
+
error,
|
|
591
|
+
pluginName: plugin.name
|
|
592
|
+
});
|
|
593
|
+
} catch {}
|
|
594
|
+
try {
|
|
595
|
+
this.lifecycle.onError?.(error, plugin);
|
|
596
|
+
} catch {}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Check if manager has any enabled plugins.
|
|
602
|
+
*
|
|
603
|
+
* @returns True if at least one plugin is enabled, false otherwise
|
|
604
|
+
*/
|
|
605
|
+
hasEnabledPlugins() {
|
|
606
|
+
return Array.from(this.plugins.values()).some((plugin) => plugin.enabled);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Get names of all enabled plugins.
|
|
610
|
+
*
|
|
611
|
+
* @returns Array of plugin names that are currently enabled
|
|
612
|
+
*/
|
|
613
|
+
getEnabledPluginNames() {
|
|
614
|
+
return Array.from(this.plugins.values()).filter((plugin) => plugin.enabled).map((plugin) => plugin.name);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region ../observability/src/factory/builder.ts
|
|
620
|
+
/**
|
|
621
|
+
* @fileoverview ObservabilityBuilder - Fluent API for building observability instances
|
|
622
|
+
* ObservabilityBuilder - Fluent API for building observability instances
|
|
623
|
+
*/
|
|
624
|
+
/**
|
|
625
|
+
* Builder for creating configured ObservabilityManager instances
|
|
626
|
+
*/
|
|
627
|
+
var ObservabilityBuilder = class ObservabilityBuilder {
|
|
628
|
+
plugins = [];
|
|
629
|
+
lifecycle = {};
|
|
630
|
+
autoInitialize = true;
|
|
631
|
+
/**
|
|
632
|
+
* Add a plugin to the observability stack
|
|
633
|
+
* @param plugin - Observability plugin to add
|
|
634
|
+
* @returns Builder instance for chaining
|
|
635
|
+
*/
|
|
636
|
+
withPlugin(plugin) {
|
|
637
|
+
if (plugin) this.plugins.push(plugin);
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Add multiple plugins at once
|
|
642
|
+
* @param plugins - Array of observability plugins to add
|
|
643
|
+
* @returns Builder instance for chaining
|
|
644
|
+
*/
|
|
645
|
+
withPlugins(plugins) {
|
|
646
|
+
if (plugins && Array.isArray(plugins)) {
|
|
647
|
+
const validPlugins = plugins.filter((plugin) => plugin != null);
|
|
648
|
+
this.plugins.push(...validPlugins);
|
|
649
|
+
}
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Set lifecycle callbacks for plugin management
|
|
654
|
+
* @param lifecycle - Lifecycle callback configuration
|
|
655
|
+
* @returns Builder instance for chaining
|
|
656
|
+
*/
|
|
657
|
+
withLifecycle(lifecycle) {
|
|
658
|
+
this.lifecycle = {
|
|
659
|
+
...this.lifecycle,
|
|
660
|
+
...lifecycle
|
|
661
|
+
};
|
|
662
|
+
return this;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Configure whether to auto-initialize plugins (default: true)
|
|
666
|
+
*/
|
|
667
|
+
withAutoInitialize(autoInitialize) {
|
|
668
|
+
this.autoInitialize = autoInitialize;
|
|
669
|
+
return this;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Build the ObservabilityManager instance
|
|
673
|
+
* @returns Configured ObservabilityManager instance
|
|
674
|
+
*/
|
|
675
|
+
build() {
|
|
676
|
+
const manager = new ObservabilityManager(this.lifecycle);
|
|
677
|
+
this.plugins.forEach((plugin) => manager.addPlugin(plugin));
|
|
678
|
+
if (this.autoInitialize && typeof process !== "undefined" && process.env.NEXT_RUNTIME !== "edge") (async () => {
|
|
679
|
+
try {
|
|
680
|
+
await manager.initialize();
|
|
681
|
+
} catch (error) {
|
|
682
|
+
try {
|
|
683
|
+
logError("Failed to initialize observability", { error });
|
|
684
|
+
} catch {}
|
|
685
|
+
}
|
|
686
|
+
})();
|
|
687
|
+
return manager;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Build and initialize the ObservabilityManager instance
|
|
691
|
+
* @returns Promise resolving to initialized ObservabilityManager
|
|
692
|
+
*/
|
|
693
|
+
async buildWithAutoInit() {
|
|
694
|
+
const manager = new ObservabilityManager(this.lifecycle);
|
|
695
|
+
this.plugins.forEach((plugin) => manager.addPlugin(plugin));
|
|
696
|
+
try {
|
|
697
|
+
await manager.initialize();
|
|
698
|
+
} catch {}
|
|
699
|
+
return manager;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Create a new builder instance
|
|
703
|
+
* @returns New ObservabilityBuilder instance
|
|
704
|
+
*/
|
|
705
|
+
static create() {
|
|
706
|
+
return new ObservabilityBuilder();
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region ../observability/src/plugins/betterstack/env.ts
|
|
712
|
+
/**
|
|
713
|
+
* @fileoverview Better Stack (Logtail) environment configuration
|
|
714
|
+
* Better Stack (Logtail) environment configuration
|
|
715
|
+
* Updated to support official @logtail packages
|
|
716
|
+
*/
|
|
717
|
+
const env$1 = createEnv({
|
|
718
|
+
server: {
|
|
719
|
+
LOGTAIL_SOURCE_TOKEN: z.string().optional(),
|
|
720
|
+
BETTERSTACK_SOURCE_TOKEN: z.string().optional(),
|
|
721
|
+
BETTER_STACK_SOURCE_TOKEN: z.string().optional(),
|
|
722
|
+
BETTER_STACK_INGESTING_URL: z.string().url().optional(),
|
|
723
|
+
BETTER_STACK_LOG_LEVEL: z.enum([
|
|
724
|
+
"debug",
|
|
725
|
+
"info",
|
|
726
|
+
"warn",
|
|
727
|
+
"error",
|
|
728
|
+
"off"
|
|
729
|
+
]).optional()
|
|
730
|
+
},
|
|
731
|
+
client: {
|
|
732
|
+
NEXT_PUBLIC_LOGTAIL_TOKEN: z.string().optional(),
|
|
733
|
+
NEXT_PUBLIC_BETTERSTACK_TOKEN: z.string().optional(),
|
|
734
|
+
NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN: z.string().optional(),
|
|
735
|
+
NEXT_PUBLIC_BETTER_STACK_INGESTING_URL: z.string().url().optional(),
|
|
736
|
+
NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL: z.enum([
|
|
737
|
+
"debug",
|
|
738
|
+
"info",
|
|
739
|
+
"warn",
|
|
740
|
+
"error",
|
|
741
|
+
"off"
|
|
742
|
+
]).optional()
|
|
743
|
+
},
|
|
744
|
+
clientPrefix: "NEXT_PUBLIC_",
|
|
745
|
+
runtimeEnv: process.env,
|
|
746
|
+
emptyStringAsUndefined: true,
|
|
747
|
+
onInvalidAccess: (variable) => {
|
|
748
|
+
throw new Error(`❌ Attempted to access a server-side environment variable on the client: ${variable}`);
|
|
749
|
+
},
|
|
750
|
+
onValidationError: (error) => {
|
|
751
|
+
logWarn("Better Stack environment validation failed", { error });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
/**
|
|
755
|
+
* Safe environment access for non-Next.js contexts.
|
|
756
|
+
*
|
|
757
|
+
* Provides fallback environment variable access for Node.js, workers, and test environments
|
|
758
|
+
* where the validated env object may not be available. Returns fallback values with proper
|
|
759
|
+
* priority handling for legacy and official Better Stack variable names.
|
|
760
|
+
*
|
|
761
|
+
* @returns Environment object with Better Stack configuration values
|
|
762
|
+
*/
|
|
763
|
+
function safeEnv$1() {
|
|
764
|
+
if (env$1) return env$1;
|
|
765
|
+
return {
|
|
766
|
+
LOGTAIL_SOURCE_TOKEN: process.env.LOGTAIL_SOURCE_TOKEN ?? process.env.BETTERSTACK_SOURCE_TOKEN ?? "",
|
|
767
|
+
BETTERSTACK_SOURCE_TOKEN: process.env.BETTERSTACK_SOURCE_TOKEN ?? process.env.LOGTAIL_SOURCE_TOKEN ?? "",
|
|
768
|
+
NEXT_PUBLIC_LOGTAIL_TOKEN: process.env.NEXT_PUBLIC_LOGTAIL_TOKEN ?? process.env.NEXT_PUBLIC_BETTERSTACK_TOKEN ?? "",
|
|
769
|
+
NEXT_PUBLIC_BETTERSTACK_TOKEN: process.env.NEXT_PUBLIC_BETTERSTACK_TOKEN ?? process.env.NEXT_PUBLIC_LOGTAIL_TOKEN ?? "",
|
|
770
|
+
BETTER_STACK_SOURCE_TOKEN: process.env.BETTER_STACK_SOURCE_TOKEN ?? process.env.LOGTAIL_SOURCE_TOKEN ?? process.env.BETTERSTACK_SOURCE_TOKEN ?? "",
|
|
771
|
+
BETTER_STACK_INGESTING_URL: process.env.BETTER_STACK_INGESTING_URL ?? "",
|
|
772
|
+
BETTER_STACK_LOG_LEVEL: process.env.BETTER_STACK_LOG_LEVEL ?? "info",
|
|
773
|
+
NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN: process.env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN ?? process.env.NEXT_PUBLIC_LOGTAIL_TOKEN ?? process.env.NEXT_PUBLIC_BETTERSTACK_TOKEN ?? "",
|
|
774
|
+
NEXT_PUBLIC_BETTER_STACK_INGESTING_URL: process.env.NEXT_PUBLIC_BETTER_STACK_INGESTING_URL ?? "",
|
|
775
|
+
NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL: process.env.NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL ?? "info"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region ../observability/src/plugins/betterstack/plugin.ts
|
|
781
|
+
/**
|
|
782
|
+
* @fileoverview Better Stack (Logtail) plugin implementation
|
|
783
|
+
* Better Stack (Logtail) plugin implementation
|
|
784
|
+
* Updated to use official @logtail packages
|
|
785
|
+
*/
|
|
786
|
+
/**
|
|
787
|
+
* Maximum number of breadcrumbs to include in error/message context
|
|
788
|
+
* Limits context size to prevent excessive data transmission
|
|
789
|
+
*/
|
|
790
|
+
const MAX_BREADCRUMBS_IN_CONTEXT = 10;
|
|
791
|
+
/**
|
|
792
|
+
* Maximum number of breadcrumbs to store in memory
|
|
793
|
+
* Prevents unbounded memory growth while maintaining useful history
|
|
794
|
+
*/
|
|
795
|
+
const MAX_BREADCRUMBS_STORED = 100;
|
|
796
|
+
/**
|
|
797
|
+
* Better Stack plugin implementation using official @logtail packages
|
|
798
|
+
*/
|
|
799
|
+
/**
|
|
800
|
+
* Better Stack (Logtail) plugin implementation.
|
|
801
|
+
*
|
|
802
|
+
* Integrates Better Stack logging service into the observability system.
|
|
803
|
+
* Uses official @logtail packages (@logtail/js or @logtail/next) for unified
|
|
804
|
+
* logging across environments.
|
|
805
|
+
*/
|
|
806
|
+
var BetterStackPlugin = class {
|
|
807
|
+
name = "betterstack";
|
|
808
|
+
enabled;
|
|
809
|
+
client;
|
|
810
|
+
initialized = false;
|
|
811
|
+
logtailPackage;
|
|
812
|
+
currentUser = null;
|
|
813
|
+
breadcrumbs = [];
|
|
814
|
+
breadcrumbIndex = 0;
|
|
815
|
+
config;
|
|
816
|
+
/**
|
|
817
|
+
* Create a new BetterStackPlugin instance.
|
|
818
|
+
*
|
|
819
|
+
* @param config - Better Stack plugin configuration
|
|
820
|
+
*/
|
|
821
|
+
constructor(config = {}) {
|
|
822
|
+
this.config = config;
|
|
823
|
+
const env = safeEnv$1();
|
|
824
|
+
const hasToken = config.sourceToken ?? env.BETTER_STACK_SOURCE_TOKEN ?? env.BETTERSTACK_SOURCE_TOKEN ?? env.LOGTAIL_SOURCE_TOKEN ?? env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN ?? env.NEXT_PUBLIC_BETTERSTACK_TOKEN ?? env.NEXT_PUBLIC_LOGTAIL_TOKEN;
|
|
825
|
+
this.enabled = config.enabled ?? Boolean(hasToken);
|
|
826
|
+
this.logtailPackage = config.logtailPackage ?? this.detectLogtailPackage();
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Detect which Logtail package to use based on the runtime environment.
|
|
830
|
+
*
|
|
831
|
+
* @returns Package name ('@logtail/next' for Next.js, '@logtail/js' otherwise)
|
|
832
|
+
*/
|
|
833
|
+
detectLogtailPackage() {
|
|
834
|
+
if (process.env.NEXT_RUNTIME) return "@logtail/next";
|
|
835
|
+
return "@logtail/js";
|
|
836
|
+
}
|
|
837
|
+
getClient() {
|
|
838
|
+
return this.client;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Get safe environment access (for testing/mocking).
|
|
842
|
+
*
|
|
843
|
+
* @returns Environment configuration object
|
|
844
|
+
*/
|
|
845
|
+
getSafeEnv() {
|
|
846
|
+
return safeEnv$1();
|
|
847
|
+
}
|
|
848
|
+
async initialize(config) {
|
|
849
|
+
if (this.initialized || !this.enabled) return;
|
|
850
|
+
const env = safeEnv$1();
|
|
851
|
+
const mergedConfig = {
|
|
852
|
+
...this.config,
|
|
853
|
+
...config
|
|
854
|
+
};
|
|
855
|
+
const sourceToken = mergedConfig.sourceToken ?? env.BETTER_STACK_SOURCE_TOKEN ?? env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN ?? env.LOGTAIL_SOURCE_TOKEN ?? env.BETTERSTACK_SOURCE_TOKEN ?? env.NEXT_PUBLIC_LOGTAIL_TOKEN ?? env.NEXT_PUBLIC_BETTERSTACK_TOKEN;
|
|
856
|
+
if (!sourceToken) {
|
|
857
|
+
logWarn("Better Stack plugin: No source token provided, skipping initialization");
|
|
858
|
+
this.enabled = false;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const LogtailModule = await import(
|
|
863
|
+
/* webpackIgnore: true */
|
|
864
|
+
this.logtailPackage
|
|
865
|
+
);
|
|
866
|
+
const LogtailClass = LogtailModule.default ?? LogtailModule.Logtail ?? LogtailModule;
|
|
867
|
+
if (!LogtailClass) {
|
|
868
|
+
logError(`Better Stack plugin: Could not find Logtail class in ${this.logtailPackage}`, { logtailPackage: this.logtailPackage });
|
|
869
|
+
this.enabled = false;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const logtailConfig = { sourceToken };
|
|
873
|
+
if (mergedConfig.endpoint ?? env.BETTER_STACK_INGESTING_URL ?? env.NEXT_PUBLIC_BETTER_STACK_INGESTING_URL) logtailConfig.endpoint = mergedConfig.endpoint ?? env.BETTER_STACK_INGESTING_URL ?? env.NEXT_PUBLIC_BETTER_STACK_INGESTING_URL;
|
|
874
|
+
this.client = new LogtailClass(logtailConfig);
|
|
875
|
+
this.initialized = true;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
logError(`Failed to import Logtail package '${this.logtailPackage}'`, {
|
|
878
|
+
error,
|
|
879
|
+
logtailPackage: this.logtailPackage
|
|
880
|
+
});
|
|
881
|
+
this.enabled = false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async shutdown() {
|
|
885
|
+
if (this.client && this.initialized) {
|
|
886
|
+
await this.client.flush();
|
|
887
|
+
this.initialized = false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
captureException(error, context) {
|
|
891
|
+
if (!this.enabled || !this.client) return;
|
|
892
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
893
|
+
const data = {
|
|
894
|
+
error: {
|
|
895
|
+
name: errorObj.name,
|
|
896
|
+
message: errorObj.message,
|
|
897
|
+
stack: errorObj.stack
|
|
898
|
+
},
|
|
899
|
+
context: context?.extra,
|
|
900
|
+
tags: context?.tags,
|
|
901
|
+
user: context?.user ?? this.currentUser,
|
|
902
|
+
breadcrumbs: this.getRecentBreadcrumbs(MAX_BREADCRUMBS_IN_CONTEXT)
|
|
903
|
+
};
|
|
904
|
+
this.client.error(errorObj.message, data);
|
|
905
|
+
}
|
|
906
|
+
captureMessage(message, level = "info", context) {
|
|
907
|
+
if (!this.enabled || !this.client) return;
|
|
908
|
+
const data = {
|
|
909
|
+
level,
|
|
910
|
+
context: context?.extra,
|
|
911
|
+
tags: context?.tags,
|
|
912
|
+
user: context?.user ?? this.currentUser,
|
|
913
|
+
breadcrumbs: this.getRecentBreadcrumbs(MAX_BREADCRUMBS_IN_CONTEXT)
|
|
914
|
+
};
|
|
915
|
+
switch (level) {
|
|
916
|
+
case "debug":
|
|
917
|
+
this.client.debug(message, data);
|
|
918
|
+
break;
|
|
919
|
+
case "warning":
|
|
920
|
+
this.client.warn(message, data);
|
|
921
|
+
break;
|
|
922
|
+
case "error":
|
|
923
|
+
this.client.error(message, data);
|
|
924
|
+
break;
|
|
925
|
+
default:
|
|
926
|
+
this.client.info(message, data);
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
setUser(user) {
|
|
931
|
+
if (!this.enabled) return;
|
|
932
|
+
this.currentUser = user;
|
|
933
|
+
}
|
|
934
|
+
addBreadcrumb(breadcrumb) {
|
|
935
|
+
if (!this.enabled) return;
|
|
936
|
+
const enriched = {
|
|
937
|
+
...breadcrumb,
|
|
938
|
+
timestamp: breadcrumb.timestamp ?? Date.now() / 1e3
|
|
939
|
+
};
|
|
940
|
+
if (this.breadcrumbs.length < MAX_BREADCRUMBS_STORED) this.breadcrumbs.push(enriched);
|
|
941
|
+
else {
|
|
942
|
+
this.breadcrumbs[this.breadcrumbIndex] = enriched;
|
|
943
|
+
this.breadcrumbIndex = (this.breadcrumbIndex + 1) % MAX_BREADCRUMBS_STORED;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Get the most recent breadcrumbs in chronological order
|
|
948
|
+
* @param count - Number of breadcrumbs to retrieve
|
|
949
|
+
* @returns Array of recent breadcrumbs
|
|
950
|
+
*/
|
|
951
|
+
getRecentBreadcrumbs(count) {
|
|
952
|
+
const { length } = this.breadcrumbs;
|
|
953
|
+
if (length === 0) return [];
|
|
954
|
+
if (length <= count) return this.breadcrumbs.slice();
|
|
955
|
+
if (length < MAX_BREADCRUMBS_STORED) return this.breadcrumbs.slice(length - count);
|
|
956
|
+
const result = [];
|
|
957
|
+
const start = (this.breadcrumbIndex - count + length) % length;
|
|
958
|
+
for (let i = 0; i < count; i++) {
|
|
959
|
+
const index = (start + i) % length;
|
|
960
|
+
const breadcrumb = this.breadcrumbs[index];
|
|
961
|
+
if (breadcrumb) result.push(breadcrumb);
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
withScope(callback) {
|
|
966
|
+
if (!this.enabled) return;
|
|
967
|
+
try {
|
|
968
|
+
callback({
|
|
969
|
+
setContext: (_key, _context) => {},
|
|
970
|
+
setUser: (user) => {
|
|
971
|
+
this.setUser(user);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
} catch {}
|
|
975
|
+
}
|
|
976
|
+
async flush(_timeout) {
|
|
977
|
+
if (!this.enabled || !this.client) return true;
|
|
978
|
+
try {
|
|
979
|
+
await this.client.flush();
|
|
980
|
+
return true;
|
|
981
|
+
} catch (error) {
|
|
982
|
+
logError("Better Stack flush error", { error });
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
/**
|
|
988
|
+
* Factory function to create a Better Stack plugin.
|
|
989
|
+
*
|
|
990
|
+
* Creates a configured BetterStackPlugin instance with optional configuration.
|
|
991
|
+
* Auto-detects the appropriate @logtail package based on the environment.
|
|
992
|
+
*
|
|
993
|
+
* @param config - Optional Better Stack plugin configuration
|
|
994
|
+
* @returns BetterStackPlugin instance
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* ```typescript
|
|
998
|
+
* const betterstack = createBetterStackPlugin({
|
|
999
|
+
* sourceToken: '...',
|
|
1000
|
+
* logLevel: 'info',
|
|
1001
|
+
* });
|
|
1002
|
+
* ```
|
|
1003
|
+
*/
|
|
1004
|
+
const createBetterStackPlugin = (config) => {
|
|
1005
|
+
return new BetterStackPlugin(config);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region ../observability/src/shared.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* @fileoverview Shared observability utilities and types
|
|
1012
|
+
* Shared observability utilities and types
|
|
1013
|
+
*/
|
|
1014
|
+
/**
|
|
1015
|
+
* Get runtime environment information.
|
|
1016
|
+
*
|
|
1017
|
+
* Detects the current runtime environment (browser, Node.js, edge, Bun) and returns
|
|
1018
|
+
* detailed information about the environment type, version, and capabilities.
|
|
1019
|
+
*
|
|
1020
|
+
* @returns Runtime information object with type, version, and environment-specific details
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```typescript
|
|
1024
|
+
* const runtime = getRuntimeInfo();
|
|
1025
|
+
* // Returns: { type: 'node', version: '22.0.0', major: 22, minor: 0, isNode22Plus: true, isNextJs: true }
|
|
1026
|
+
* // or: { type: 'browser', isNextJs: true }
|
|
1027
|
+
* // or: { type: 'edge', variant: 'vercel' }
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
const getRuntimeInfo = () => {
|
|
1031
|
+
if (typeof globalThis !== "undefined" && globalThis.EdgeRuntime) return {
|
|
1032
|
+
type: "edge",
|
|
1033
|
+
variant: "vercel"
|
|
1034
|
+
};
|
|
1035
|
+
if (typeof globalThis !== "undefined" && globalThis.caches && typeof globalThis.caches !== "undefined") return {
|
|
1036
|
+
type: "edge",
|
|
1037
|
+
variant: "cloudflare"
|
|
1038
|
+
};
|
|
1039
|
+
if (typeof process !== "undefined" && process.versions?.bun) return {
|
|
1040
|
+
type: "bun",
|
|
1041
|
+
version: process.versions.bun
|
|
1042
|
+
};
|
|
1043
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis && typeof globalThis.window !== "undefined" && typeof globalThis.window.document !== "undefined" && typeof globalThis.window.navigator !== "undefined") return {
|
|
1044
|
+
type: "browser",
|
|
1045
|
+
isNextJs: Boolean(globalThis.window.__NEXT_DATA__)
|
|
1046
|
+
};
|
|
1047
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1048
|
+
const nodeVersion = parseInt(process.versions.node.split(".")[0] ?? "0");
|
|
1049
|
+
const nodeVersionMinor = parseInt(process.versions.node.split(".")[1] ?? "0");
|
|
1050
|
+
if (nodeVersion < 22) logWarn(`[Observability] Node ${process.versions.node} detected. Node 22+ is required for optimal performance and latest features.`);
|
|
1051
|
+
return {
|
|
1052
|
+
type: "node",
|
|
1053
|
+
version: process.versions.node,
|
|
1054
|
+
major: nodeVersion,
|
|
1055
|
+
minor: nodeVersionMinor,
|
|
1056
|
+
isNode22Plus: nodeVersion >= 22,
|
|
1057
|
+
isNextJs: Boolean(process.env.NEXT_RUNTIME) || Boolean(process.env.__NEXT_RUNTIME) || Boolean(process.env.NEXT_PUBLIC_VERCEL_ENV)
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return { type: "unknown" };
|
|
1061
|
+
};
|
|
1062
|
+
const runtimeInfo = getRuntimeInfo();
|
|
1063
|
+
/**
|
|
1064
|
+
* Detect if code is running in a browser environment
|
|
1065
|
+
* Extracted to shared utility to avoid duplication
|
|
1066
|
+
* @returns true if running in browser, false otherwise
|
|
1067
|
+
*/
|
|
1068
|
+
function isBrowser() {
|
|
1069
|
+
return typeof globalThis !== "undefined" && "window" in globalThis && typeof globalThis.window !== "undefined" && typeof globalThis.window.document !== "undefined" && typeof globalThis.window.navigator !== "undefined";
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Determine if console logging should be enabled based on environment
|
|
1073
|
+
* Centralized logic to avoid duplication across entry points
|
|
1074
|
+
* @param envNodeEnv - The NEXT_PUBLIC_NODE_ENV value
|
|
1075
|
+
* @param consoleEnabled - The NEXT_PUBLIC_OBSERVABILITY_CONSOLE_ENABLED value (explicit control)
|
|
1076
|
+
* @param debugEnabled - The NEXT_PUBLIC_OBSERVABILITY_DEBUG value
|
|
1077
|
+
* @returns boolean indicating if console should be enabled
|
|
1078
|
+
*/
|
|
1079
|
+
function shouldEnableConsole(envNodeEnv, consoleEnabled, debugEnabled) {
|
|
1080
|
+
const isDevelopment = envNodeEnv === "development" || true;
|
|
1081
|
+
if (consoleEnabled !== void 0) return consoleEnabled;
|
|
1082
|
+
if (isDevelopment) return true;
|
|
1083
|
+
return debugEnabled ?? false;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region ../observability/src/plugins/console/index.ts
|
|
1088
|
+
/**
|
|
1089
|
+
* @fileoverview Console plugin for observability
|
|
1090
|
+
* Console plugin for observability
|
|
1091
|
+
* Simple logging implementation for development
|
|
1092
|
+
*/
|
|
1093
|
+
/**
|
|
1094
|
+
* Console plugin implementation for browser environments
|
|
1095
|
+
*/
|
|
1096
|
+
/**
|
|
1097
|
+
* Console plugin implementation for browser environments.
|
|
1098
|
+
*
|
|
1099
|
+
* Provides simple console logging with color support for development.
|
|
1100
|
+
* Supports both browser (CSS colors) and Node.js (ANSI colors) environments.
|
|
1101
|
+
*/
|
|
1102
|
+
var ConsolePlugin = class {
|
|
1103
|
+
name = "console";
|
|
1104
|
+
enabled;
|
|
1105
|
+
prefix;
|
|
1106
|
+
colors;
|
|
1107
|
+
isBrowser;
|
|
1108
|
+
/**
|
|
1109
|
+
* Create a new ConsolePlugin instance.
|
|
1110
|
+
*
|
|
1111
|
+
* @param config - Console plugin configuration
|
|
1112
|
+
*/
|
|
1113
|
+
constructor(config = {}) {
|
|
1114
|
+
this.enabled = config.enabled ?? true;
|
|
1115
|
+
this.prefix = config.prefix ?? "[Console]";
|
|
1116
|
+
this.colors = config.colors ?? true;
|
|
1117
|
+
this.isBrowser = isBrowser();
|
|
1118
|
+
}
|
|
1119
|
+
async initialize() {}
|
|
1120
|
+
async shutdown() {}
|
|
1121
|
+
getClient() {}
|
|
1122
|
+
captureException(error, context) {
|
|
1123
|
+
if (!this.enabled) return;
|
|
1124
|
+
try {
|
|
1125
|
+
const contextData = this.formatContext(context);
|
|
1126
|
+
if (this.colors && this.isBrowser) console.error(`%c${this.prefix} Error:`, "color: red; font-weight: bold", error, contextData);
|
|
1127
|
+
else if (this.colors) console.error(`\x1b[31m${this.prefix} Error:\x1b[0m`, error, contextData);
|
|
1128
|
+
else console.error(`${this.prefix} Error:`, error, contextData);
|
|
1129
|
+
} catch {}
|
|
1130
|
+
}
|
|
1131
|
+
captureMessage(message, level = "info", context) {
|
|
1132
|
+
if (!this.enabled) return;
|
|
1133
|
+
try {
|
|
1134
|
+
const contextData = this.formatContext(context);
|
|
1135
|
+
const logMethod = this.getLogMethod(level);
|
|
1136
|
+
const levelLabel = this.getLevelLabel(level);
|
|
1137
|
+
if (this.colors && this.isBrowser) {
|
|
1138
|
+
const cssColor = this.getBrowserColorCSS(level);
|
|
1139
|
+
console[logMethod](`%c${this.prefix} ${levelLabel}:`, cssColor, message, contextData);
|
|
1140
|
+
} else if (this.colors) {
|
|
1141
|
+
const ansiColor = this.getAnsiColorCode(level);
|
|
1142
|
+
console[logMethod](`${ansiColor}${this.prefix} ${levelLabel}:\x1b[0m`, message, contextData);
|
|
1143
|
+
} else console[logMethod](`${this.prefix} ${levelLabel}:`, message, contextData);
|
|
1144
|
+
} catch {}
|
|
1145
|
+
}
|
|
1146
|
+
setUser(user) {
|
|
1147
|
+
if (!this.enabled) return;
|
|
1148
|
+
try {
|
|
1149
|
+
if (user === null) console.info(this.prefix, "User cleared");
|
|
1150
|
+
else console.info(this.prefix, "User set:", JSON.stringify(user));
|
|
1151
|
+
} catch {}
|
|
1152
|
+
}
|
|
1153
|
+
addBreadcrumb(breadcrumb) {
|
|
1154
|
+
if (!this.enabled) return;
|
|
1155
|
+
try {
|
|
1156
|
+
const enrichedBreadcrumb = {
|
|
1157
|
+
...breadcrumb,
|
|
1158
|
+
timestamp: breadcrumb.timestamp ?? Date.now() / 1e3
|
|
1159
|
+
};
|
|
1160
|
+
console.log(this.prefix, "Breadcrumb:", JSON.stringify(enrichedBreadcrumb));
|
|
1161
|
+
} catch {}
|
|
1162
|
+
}
|
|
1163
|
+
withScope(callback) {
|
|
1164
|
+
if (!this.enabled) return;
|
|
1165
|
+
try {
|
|
1166
|
+
callback({
|
|
1167
|
+
setContext: (key, context) => {
|
|
1168
|
+
console.log(this.prefix, "Context set:", key, JSON.stringify(context));
|
|
1169
|
+
},
|
|
1170
|
+
setUser: (user) => {
|
|
1171
|
+
this.setUser(user);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
} catch {}
|
|
1175
|
+
}
|
|
1176
|
+
async flush() {
|
|
1177
|
+
if (this.enabled) console.log(this.prefix, "Flushed");
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
formatContext(context) {
|
|
1181
|
+
if (!context) return {};
|
|
1182
|
+
if (context.extra !== void 0 || context.tags !== void 0) return {
|
|
1183
|
+
context: context.extra,
|
|
1184
|
+
tags: context.tags
|
|
1185
|
+
};
|
|
1186
|
+
if ("operation" in context || "metadata" in context) return {
|
|
1187
|
+
context: context.metadata ?? {},
|
|
1188
|
+
tags: { operation: context.operation }
|
|
1189
|
+
};
|
|
1190
|
+
return {
|
|
1191
|
+
context,
|
|
1192
|
+
tags: {}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Get CSS color style for browser console
|
|
1197
|
+
* @param level - Log level
|
|
1198
|
+
* @returns CSS style string
|
|
1199
|
+
*/
|
|
1200
|
+
getBrowserColorCSS(level) {
|
|
1201
|
+
return {
|
|
1202
|
+
debug: "color: #00CED1; font-weight: bold",
|
|
1203
|
+
info: "color: #32CD32; font-weight: bold",
|
|
1204
|
+
warning: "color: #FFA500; font-weight: bold",
|
|
1205
|
+
error: "color: #DC143C; font-weight: bold"
|
|
1206
|
+
}[level] ?? "color: inherit";
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get ANSI color code for Node.js terminal
|
|
1210
|
+
* @param level - Log level
|
|
1211
|
+
* @returns ANSI escape code
|
|
1212
|
+
*/
|
|
1213
|
+
getAnsiColorCode(level) {
|
|
1214
|
+
return {
|
|
1215
|
+
debug: "\x1B[36m",
|
|
1216
|
+
info: "\x1B[32m",
|
|
1217
|
+
warning: "\x1B[33m",
|
|
1218
|
+
error: "\x1B[31m"
|
|
1219
|
+
}[level] ?? "";
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Get the console method name for a log level.
|
|
1223
|
+
*
|
|
1224
|
+
* @param level - Log level
|
|
1225
|
+
* @returns Console method name ('debug', 'info', 'warn', or 'error')
|
|
1226
|
+
*/
|
|
1227
|
+
getLogMethod(level) {
|
|
1228
|
+
switch (level) {
|
|
1229
|
+
case "debug": return "debug";
|
|
1230
|
+
case "info": return "info";
|
|
1231
|
+
case "warning": return "warn";
|
|
1232
|
+
case "error": return "error";
|
|
1233
|
+
default: return "info";
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Get a human-readable label for a log level.
|
|
1238
|
+
*
|
|
1239
|
+
* @param level - Log level
|
|
1240
|
+
* @returns Capitalized level label ('Debug', 'Info', 'Warning', 'Error')
|
|
1241
|
+
*/
|
|
1242
|
+
getLevelLabel(level) {
|
|
1243
|
+
switch (level) {
|
|
1244
|
+
case "debug": return "Debug";
|
|
1245
|
+
case "info": return "Info";
|
|
1246
|
+
case "warning": return "Warning";
|
|
1247
|
+
case "error": return "Error";
|
|
1248
|
+
default: return "Info";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
/**
|
|
1253
|
+
* Factory function to create a console plugin for browser environments.
|
|
1254
|
+
*
|
|
1255
|
+
* @param config - Console plugin configuration
|
|
1256
|
+
* @returns ConsolePlugin instance configured for browser
|
|
1257
|
+
*/
|
|
1258
|
+
const createConsolePlugin = (config) => {
|
|
1259
|
+
return new ConsolePlugin(config);
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region ../observability/src/plugins/sentry/env.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* @fileoverview Sentry-specific environment configuration
|
|
1266
|
+
* Sentry-specific environment configuration
|
|
1267
|
+
* Apps can extend this configuration to inherit Sentry environment variables
|
|
1268
|
+
*/
|
|
1269
|
+
const env = createEnv({
|
|
1270
|
+
server: {
|
|
1271
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
1272
|
+
SENTRY_AUTH_TOKEN: z.string().optional(),
|
|
1273
|
+
SENTRY_ORG: z.string().optional(),
|
|
1274
|
+
SENTRY_PROJECT: z.string().optional(),
|
|
1275
|
+
SENTRY_ENVIRONMENT: z.enum([
|
|
1276
|
+
"development",
|
|
1277
|
+
"preview",
|
|
1278
|
+
"production"
|
|
1279
|
+
]).optional(),
|
|
1280
|
+
SENTRY_RELEASE: z.string().optional(),
|
|
1281
|
+
SENTRY_TRACES_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
|
|
1282
|
+
SENTRY_PROFILES_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
|
|
1283
|
+
SENTRY_REPLAYS_SESSION_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
|
|
1284
|
+
SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
|
|
1285
|
+
SENTRY_DEBUG: z.string().optional().transform((val) => val === "true").default(false)
|
|
1286
|
+
},
|
|
1287
|
+
clientPrefix: "NEXT_PUBLIC_",
|
|
1288
|
+
client: {
|
|
1289
|
+
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
|
1290
|
+
NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.enum([
|
|
1291
|
+
"development",
|
|
1292
|
+
"preview",
|
|
1293
|
+
"production"
|
|
1294
|
+
]).optional(),
|
|
1295
|
+
NEXT_PUBLIC_SENTRY_RELEASE: z.string().optional()
|
|
1296
|
+
},
|
|
1297
|
+
runtimeEnv: process.env,
|
|
1298
|
+
emptyStringAsUndefined: true,
|
|
1299
|
+
onInvalidAccess: (variable) => {
|
|
1300
|
+
throw new Error(`❌ Attempted to access a server-side environment variable on the client: ${variable}`);
|
|
1301
|
+
},
|
|
1302
|
+
onValidationError: (error) => {
|
|
1303
|
+
logWarn("Sentry environment validation failed", { error });
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
/**
|
|
1307
|
+
* Safe environment access for non-Next.js contexts.
|
|
1308
|
+
*
|
|
1309
|
+
* Provides fallback environment variable access for Node.js, workers, and test environments
|
|
1310
|
+
* where the validated env object may not be available. Returns fallback values with proper
|
|
1311
|
+
* type coercion and defaults.
|
|
1312
|
+
*
|
|
1313
|
+
* @returns Environment object with Sentry configuration values
|
|
1314
|
+
*/
|
|
1315
|
+
function safeEnv() {
|
|
1316
|
+
if (env) return env;
|
|
1317
|
+
return {
|
|
1318
|
+
SENTRY_DSN: process.env.SENTRY_DSN ?? "",
|
|
1319
|
+
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN ?? "",
|
|
1320
|
+
SENTRY_ORG: process.env.SENTRY_ORG ?? "",
|
|
1321
|
+
SENTRY_PROJECT: process.env.SENTRY_PROJECT ?? "",
|
|
1322
|
+
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? "development",
|
|
1323
|
+
SENTRY_RELEASE: process.env.SENTRY_RELEASE ?? "",
|
|
1324
|
+
SENTRY_TRACES_SAMPLE_RATE: Number(process.env.SENTRY_TRACES_SAMPLE_RATE) || 1,
|
|
1325
|
+
SENTRY_PROFILES_SAMPLE_RATE: Number(process.env.SENTRY_PROFILES_SAMPLE_RATE) ?? 0,
|
|
1326
|
+
SENTRY_REPLAYS_SESSION_SAMPLE_RATE: Number(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE) || 0,
|
|
1327
|
+
SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: Number(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE) || 1,
|
|
1328
|
+
SENTRY_DEBUG: process.env.SENTRY_DEBUG === "true",
|
|
1329
|
+
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN ?? "",
|
|
1330
|
+
NEXT_PUBLIC_SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "development",
|
|
1331
|
+
NEXT_PUBLIC_SENTRY_RELEASE: process.env.NEXT_PUBLIC_SENTRY_RELEASE ?? ""
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region ../observability/src/plugins/sentry/plugin.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* @fileoverview Sentry plugin implementation
|
|
1339
|
+
* Sentry plugin implementation
|
|
1340
|
+
*/
|
|
1341
|
+
/**
|
|
1342
|
+
* Default log filter function that filters out noisy development logs.
|
|
1343
|
+
* Filters out:
|
|
1344
|
+
* - Next.js Fast Refresh messages
|
|
1345
|
+
* - HMR (Hot Module Replacement) messages
|
|
1346
|
+
* - Prisma query logs (server-only)
|
|
1347
|
+
* - Prisma engine/info logs
|
|
1348
|
+
* - SQL queries matching Prisma's format
|
|
1349
|
+
*
|
|
1350
|
+
* Handles ANSI color codes, console formatting (%c, %s), and CSS styling.
|
|
1351
|
+
*
|
|
1352
|
+
* @param log - The log entry from Sentry
|
|
1353
|
+
* @param isServer - Whether this is running on the server (defaults to true)
|
|
1354
|
+
* @returns The log entry if it should be sent, or null to filter it out
|
|
1355
|
+
*/
|
|
1356
|
+
function defaultBeforeSendLog(log, isServer = true) {
|
|
1357
|
+
const messageToCheck = log.message || log.formatted || "";
|
|
1358
|
+
if (!messageToCheck) return log;
|
|
1359
|
+
const messageWithoutAnsi = String(messageToCheck).replace(/\u001b\[[0-9;]*m/g, "").replace(/%[csdfiO]/g, "").replace(/background:[^;]+;?/g, "").replace(/color:[^;]+;?/g, "").replace(/border-radius:[^;]+;?/g, "").replace(/light-dark\([^)]+\)/g, "").replace(/rgba?\([^)]+\)/g, "").replace(/#[0-9a-fA-F]{3,8}/g, "").toLowerCase();
|
|
1360
|
+
if (messageWithoutAnsi.includes("[fast refresh]") || messageWithoutAnsi.includes("[hmr]")) return null;
|
|
1361
|
+
if (isServer) {
|
|
1362
|
+
if (messageWithoutAnsi.includes("prisma:query") || messageWithoutAnsi.includes("prisma:engine") || messageWithoutAnsi.includes("prisma:info") || messageWithoutAnsi.includes("select") && messageWithoutAnsi.includes("from") && (messageWithoutAnsi.includes("\"public\".") || messageWithoutAnsi.includes("public."))) return null;
|
|
1363
|
+
}
|
|
1364
|
+
return log;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Sentry plugin implementation
|
|
1368
|
+
*/
|
|
1369
|
+
/**
|
|
1370
|
+
* Sentry plugin implementation.
|
|
1371
|
+
*
|
|
1372
|
+
* Integrates Sentry error tracking and performance monitoring into the observability system.
|
|
1373
|
+
* Auto-detects the appropriate Sentry package (@sentry/node, @sentry/browser, @sentry/nextjs)
|
|
1374
|
+
* based on the runtime environment.
|
|
1375
|
+
*/
|
|
1376
|
+
var SentryPlugin = class {
|
|
1377
|
+
name = "sentry";
|
|
1378
|
+
enabled;
|
|
1379
|
+
client;
|
|
1380
|
+
initialized = false;
|
|
1381
|
+
sentryPackage;
|
|
1382
|
+
config;
|
|
1383
|
+
/**
|
|
1384
|
+
* Create a new SentryPlugin instance.
|
|
1385
|
+
*
|
|
1386
|
+
* @param config - Sentry plugin configuration
|
|
1387
|
+
*/
|
|
1388
|
+
constructor(config = {}) {
|
|
1389
|
+
this.config = config;
|
|
1390
|
+
const env = safeEnv();
|
|
1391
|
+
const hasDSN = config.dsn ?? env.SENTRY_DSN ?? env.NEXT_PUBLIC_SENTRY_DSN;
|
|
1392
|
+
this.enabled = config.enabled ?? Boolean(hasDSN);
|
|
1393
|
+
this.sentryPackage = config.sentryPackage ?? this.detectSentryPackage();
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Detect which Sentry package to use based on the runtime environment.
|
|
1397
|
+
*
|
|
1398
|
+
* @returns Package name ('@sentry/node', '@sentry/browser', or '@sentry/nextjs')
|
|
1399
|
+
*/
|
|
1400
|
+
detectSentryPackage() {
|
|
1401
|
+
if (isBrowser()) return "@sentry/browser";
|
|
1402
|
+
else if (process.env.NEXT_RUNTIME === "edge") return "@sentry/nextjs";
|
|
1403
|
+
else if (process.env.NEXT_RUNTIME) return "@sentry/nextjs";
|
|
1404
|
+
else return "@sentry/node";
|
|
1405
|
+
}
|
|
1406
|
+
getClient() {
|
|
1407
|
+
return this.client;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get safe environment access (for testing/mocking).
|
|
1411
|
+
*
|
|
1412
|
+
* @returns Environment configuration object
|
|
1413
|
+
*/
|
|
1414
|
+
getSafeEnv() {
|
|
1415
|
+
return safeEnv();
|
|
1416
|
+
}
|
|
1417
|
+
async initialize(config) {
|
|
1418
|
+
if (this.initialized || !this.enabled) return;
|
|
1419
|
+
const env = safeEnv();
|
|
1420
|
+
const mergedConfig = {
|
|
1421
|
+
...this.config,
|
|
1422
|
+
...config
|
|
1423
|
+
};
|
|
1424
|
+
const dsn = mergedConfig.dsn ?? env.SENTRY_DSN ?? env.NEXT_PUBLIC_SENTRY_DSN;
|
|
1425
|
+
if (!dsn) {
|
|
1426
|
+
logWarn("Sentry plugin: No DSN provided, skipping initialization");
|
|
1427
|
+
this.enabled = false;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
try {
|
|
1431
|
+
this.client = await import(
|
|
1432
|
+
/* webpackIgnore: true */
|
|
1433
|
+
this.sentryPackage
|
|
1434
|
+
);
|
|
1435
|
+
if (this.client && this.enabled) {
|
|
1436
|
+
let { integrations } = mergedConfig;
|
|
1437
|
+
if (!integrations && this.client.browserTracingIntegration) {
|
|
1438
|
+
integrations = [];
|
|
1439
|
+
if (mergedConfig.tracesSampleRate !== void 0 || env.SENTRY_TRACES_SAMPLE_RATE !== void 0) integrations.push(this.client.browserTracingIntegration());
|
|
1440
|
+
if (this.client.replayIntegration && (mergedConfig.replaysSessionSampleRate !== void 0 || env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE !== void 0)) integrations.push(this.client.replayIntegration());
|
|
1441
|
+
if (this.client.profilesIntegration && (mergedConfig.profilesSampleRate !== void 0 || env.SENTRY_PROFILES_SAMPLE_RATE !== void 0)) integrations.push(this.client.profilesIntegration());
|
|
1442
|
+
}
|
|
1443
|
+
const enableLogs = mergedConfig.enableLogs ?? true;
|
|
1444
|
+
if (enableLogs && this.client.consoleLoggingIntegration) {
|
|
1445
|
+
const consoleLoggingConfig = mergedConfig.consoleLoggingIntegration || { levels: [
|
|
1446
|
+
"log",
|
|
1447
|
+
"warn",
|
|
1448
|
+
"error"
|
|
1449
|
+
] };
|
|
1450
|
+
if (!integrations) integrations = [];
|
|
1451
|
+
integrations.push(this.client.consoleLoggingIntegration(consoleLoggingConfig));
|
|
1452
|
+
}
|
|
1453
|
+
this.client.init({
|
|
1454
|
+
dsn,
|
|
1455
|
+
environment: mergedConfig.environment ?? env.SENTRY_ENVIRONMENT ?? "development",
|
|
1456
|
+
release: mergedConfig.release ?? env.SENTRY_RELEASE,
|
|
1457
|
+
debug: mergedConfig.debug ?? env.SENTRY_DEBUG,
|
|
1458
|
+
enableLogs,
|
|
1459
|
+
tracesSampleRate: mergedConfig.tracesSampleRate ?? env.SENTRY_TRACES_SAMPLE_RATE,
|
|
1460
|
+
profilesSampleRate: mergedConfig.profilesSampleRate ?? env.SENTRY_PROFILES_SAMPLE_RATE,
|
|
1461
|
+
replaysSessionSampleRate: mergedConfig.replaysSessionSampleRate ?? env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
|
|
1462
|
+
replaysOnErrorSampleRate: mergedConfig.replaysOnErrorSampleRate ?? env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
|
|
1463
|
+
integrations,
|
|
1464
|
+
beforeSend: mergedConfig.beforeSend,
|
|
1465
|
+
beforeSendTransaction: mergedConfig.beforeSendTransaction,
|
|
1466
|
+
beforeSendLog: mergedConfig.beforeSendLog || (enableLogs ? (log) => defaultBeforeSendLog(log, !isBrowser()) : void 0),
|
|
1467
|
+
tracePropagationTargets: mergedConfig.tracePropagationTargets,
|
|
1468
|
+
initialScope: mergedConfig.initialScope,
|
|
1469
|
+
maxBreadcrumbs: mergedConfig.maxBreadcrumbs,
|
|
1470
|
+
attachStacktrace: mergedConfig.attachStacktrace,
|
|
1471
|
+
autoSessionTracking: mergedConfig.autoSessionTracking
|
|
1472
|
+
});
|
|
1473
|
+
this.initialized = true;
|
|
1474
|
+
}
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
logError(`Failed to import Sentry package '${this.sentryPackage}'`, { error });
|
|
1477
|
+
this.enabled = false;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async shutdown() {
|
|
1481
|
+
if (this.client && this.initialized) {
|
|
1482
|
+
if (this.client.close) await this.client.close();
|
|
1483
|
+
else if (this.client.flush) await this.client.flush();
|
|
1484
|
+
this.initialized = false;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Clean up the plugin (alias for shutdown)
|
|
1489
|
+
*/
|
|
1490
|
+
async cleanup() {
|
|
1491
|
+
await this.shutdown();
|
|
1492
|
+
}
|
|
1493
|
+
captureException(error, context) {
|
|
1494
|
+
if (!this.enabled || !this.client) return;
|
|
1495
|
+
if (typeof this.client.captureException !== "function") {
|
|
1496
|
+
logError("[Sentry Fallback] Exception", { error });
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
this.client.captureException(error, context);
|
|
1501
|
+
} catch (captureError) {
|
|
1502
|
+
logWarn("[Sentry Plugin] Failed to capture exception", { error: captureError });
|
|
1503
|
+
logError("[Sentry Fallback] Exception", { error });
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
captureMessage(message, level = "info", context) {
|
|
1507
|
+
if (!this.enabled || !this.client) return;
|
|
1508
|
+
if (this.client.logger) try {
|
|
1509
|
+
const attributes = context ? context.extra ?? context : void 0;
|
|
1510
|
+
switch (level) {
|
|
1511
|
+
case "debug":
|
|
1512
|
+
this.client.logger.debug(message, attributes);
|
|
1513
|
+
return;
|
|
1514
|
+
case "info":
|
|
1515
|
+
this.client.logger.info(message, attributes);
|
|
1516
|
+
return;
|
|
1517
|
+
case "warning":
|
|
1518
|
+
this.client.logger.warn(message, attributes);
|
|
1519
|
+
return;
|
|
1520
|
+
case "error":
|
|
1521
|
+
this.client.logger.error(message, attributes);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
logWarn("[Sentry Plugin] Failed to log via logger", { error });
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof this.client.captureMessage !== "function") {
|
|
1528
|
+
if (level === "error") logError(`[Sentry Fallback] ${message}`);
|
|
1529
|
+
else if (level === "warning") logWarn(`[Sentry Fallback] ${message}`);
|
|
1530
|
+
else logDebug(`[Sentry Fallback] ${message}`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const sentryLevel = level === "warning" ? "warning" : level === "error" ? "error" : level === "debug" ? "debug" : "info";
|
|
1534
|
+
try {
|
|
1535
|
+
const captureContext = context ? {
|
|
1536
|
+
level: sentryLevel,
|
|
1537
|
+
extra: context.extra ?? context,
|
|
1538
|
+
tags: context.tags
|
|
1539
|
+
} : sentryLevel;
|
|
1540
|
+
this.client.captureMessage(message, captureContext);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
logWarn("[Sentry Plugin] Failed to capture message", { error });
|
|
1543
|
+
if (level === "error") logError(`[Sentry Fallback] ${message}`);
|
|
1544
|
+
else if (level === "warning") logWarn(`[Sentry Fallback] ${message}`);
|
|
1545
|
+
else logDebug(`[Sentry Fallback] ${message}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
setUser(user) {
|
|
1549
|
+
if (!this.enabled || !this.client) return;
|
|
1550
|
+
this.client.setUser(user);
|
|
1551
|
+
}
|
|
1552
|
+
addBreadcrumb(breadcrumb) {
|
|
1553
|
+
if (!this.enabled || !this.client) return;
|
|
1554
|
+
this.client.addBreadcrumb({
|
|
1555
|
+
...breadcrumb,
|
|
1556
|
+
timestamp: breadcrumb.timestamp ?? Date.now() / 1e3
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
withScope(callback) {
|
|
1560
|
+
if (!this.enabled || !this.client) return;
|
|
1561
|
+
this.client.withScope(callback);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Start a new transaction
|
|
1565
|
+
*/
|
|
1566
|
+
startTransaction(context, customSamplingContext) {
|
|
1567
|
+
if (!this.enabled || !this.client) return void 0;
|
|
1568
|
+
if (this.client.startTransaction) return this.client.startTransaction(context, customSamplingContext);
|
|
1569
|
+
logWarn("startTransaction not available in this Sentry version");
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Start a new span
|
|
1573
|
+
*/
|
|
1574
|
+
startSpan(context) {
|
|
1575
|
+
if (!this.enabled || !this.client) return void 0;
|
|
1576
|
+
if (this.client.startSpan) return this.client.startSpan(context);
|
|
1577
|
+
const transaction = this.getActiveTransaction();
|
|
1578
|
+
if (transaction?.startChild) return transaction.startChild(context);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Get the currently active transaction
|
|
1582
|
+
*/
|
|
1583
|
+
getActiveTransaction() {
|
|
1584
|
+
if (!this.enabled || !this.client) return void 0;
|
|
1585
|
+
if (this.client.getActiveTransaction) return this.client.getActiveTransaction();
|
|
1586
|
+
if (this.client.getCurrentScope) {
|
|
1587
|
+
const scope = this.client.getCurrentScope();
|
|
1588
|
+
if (scope?.getTransaction) return scope.getTransaction();
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Configure the current scope
|
|
1593
|
+
*/
|
|
1594
|
+
configureScope(callback) {
|
|
1595
|
+
if (!this.enabled || !this.client) return;
|
|
1596
|
+
if (this.client.configureScope) this.client.configureScope(callback);
|
|
1597
|
+
else if (this.client.withScope) this.client.withScope(callback);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Get the current hub instance
|
|
1601
|
+
*/
|
|
1602
|
+
getCurrentHub() {
|
|
1603
|
+
if (!this.enabled || !this.client) return void 0;
|
|
1604
|
+
if (this.client.getCurrentHub) return this.client.getCurrentHub();
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Set a measurement on the active transaction
|
|
1608
|
+
*/
|
|
1609
|
+
setMeasurement(name, value, unit) {
|
|
1610
|
+
if (!this.enabled || !this.client) return;
|
|
1611
|
+
const transaction = this.getActiveTransaction();
|
|
1612
|
+
if (transaction?.setMeasurement) transaction.setMeasurement(name, value, unit);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Add performance entries as breadcrumbs and measurements
|
|
1616
|
+
*/
|
|
1617
|
+
addPerformanceEntries(entries) {
|
|
1618
|
+
if (!this.enabled || !this.client) return;
|
|
1619
|
+
const transaction = this.getActiveTransaction();
|
|
1620
|
+
entries.forEach((entry) => {
|
|
1621
|
+
this.addBreadcrumb({
|
|
1622
|
+
category: "performance",
|
|
1623
|
+
message: `${entry.entryType}: ${entry.name}`,
|
|
1624
|
+
data: {
|
|
1625
|
+
name: entry.name,
|
|
1626
|
+
duration: entry.duration,
|
|
1627
|
+
startTime: entry.startTime,
|
|
1628
|
+
...entry.entryType === "navigation" && {
|
|
1629
|
+
transferSize: entry.transferSize,
|
|
1630
|
+
encodedBodySize: entry.encodedBodySize,
|
|
1631
|
+
decodedBodySize: entry.decodedBodySize
|
|
1632
|
+
},
|
|
1633
|
+
...entry.entryType === "resource" && {
|
|
1634
|
+
initiatorType: entry.initiatorType,
|
|
1635
|
+
transferSize: entry.transferSize
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
if (transaction && entry.entryType === "navigation") {
|
|
1640
|
+
const navEntry = entry;
|
|
1641
|
+
const fetchStart = navEntry.fetchStart ?? 0;
|
|
1642
|
+
if (navEntry.responseStart !== void 0) this.setMeasurement("fcp", navEntry.responseStart - fetchStart, "millisecond");
|
|
1643
|
+
if (navEntry.domInteractive !== void 0) this.setMeasurement("dom_interactive", navEntry.domInteractive - fetchStart, "millisecond");
|
|
1644
|
+
if (navEntry.domComplete !== void 0) this.setMeasurement("dom_complete", navEntry.domComplete - fetchStart, "millisecond");
|
|
1645
|
+
if (navEntry.loadEventEnd !== void 0) this.setMeasurement("load_event_end", navEntry.loadEventEnd - fetchStart, "millisecond");
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Record a Web Vital measurement
|
|
1651
|
+
*/
|
|
1652
|
+
recordWebVital(name, value, options) {
|
|
1653
|
+
if (!this.enabled || !this.client) return;
|
|
1654
|
+
const { unit = "millisecond", rating } = options ?? {};
|
|
1655
|
+
const transaction = this.getActiveTransaction();
|
|
1656
|
+
if (transaction) {
|
|
1657
|
+
transaction.setMeasurement(name.toLowerCase(), value, unit);
|
|
1658
|
+
if (rating) transaction.setTag(`webvital.${name.toLowerCase()}.rating`, rating);
|
|
1659
|
+
this.client.captureMessage(`Web Vital: ${name}`, {
|
|
1660
|
+
level: "info",
|
|
1661
|
+
tags: {
|
|
1662
|
+
"webvital.name": name,
|
|
1663
|
+
"webvital.value": value,
|
|
1664
|
+
"webvital.unit": unit,
|
|
1665
|
+
...rating && { "webvital.rating": rating }
|
|
1666
|
+
},
|
|
1667
|
+
contexts: { trace: {
|
|
1668
|
+
trace_id: transaction.traceId,
|
|
1669
|
+
span_id: transaction.spanId
|
|
1670
|
+
} }
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Create a custom performance mark
|
|
1676
|
+
*/
|
|
1677
|
+
mark(name, options) {
|
|
1678
|
+
if (!this.enabled) return;
|
|
1679
|
+
if (typeof performance !== "undefined" && performance.mark) performance.mark(name, options);
|
|
1680
|
+
this.addBreadcrumb({
|
|
1681
|
+
category: "performance.mark",
|
|
1682
|
+
message: name,
|
|
1683
|
+
data: options?.detail,
|
|
1684
|
+
timestamp: Date.now() / 1e3
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Create a custom performance measure
|
|
1689
|
+
*/
|
|
1690
|
+
measure(name, startMarkOrOptions, endMark) {
|
|
1691
|
+
if (!this.enabled) return;
|
|
1692
|
+
if (typeof performance !== "undefined" && performance.measure) {
|
|
1693
|
+
if (typeof startMarkOrOptions === "string") performance.measure(name, startMarkOrOptions, endMark);
|
|
1694
|
+
else if (startMarkOrOptions) performance.measure(name, startMarkOrOptions);
|
|
1695
|
+
else performance.measure(name);
|
|
1696
|
+
const measures = performance.getEntriesByName(name, "measure");
|
|
1697
|
+
const measure = measures[measures.length - 1];
|
|
1698
|
+
if (measure) {
|
|
1699
|
+
this.setMeasurement(name, measure.duration, "millisecond");
|
|
1700
|
+
this.addBreadcrumb({
|
|
1701
|
+
category: "performance.measure",
|
|
1702
|
+
message: name,
|
|
1703
|
+
data: {
|
|
1704
|
+
duration: measure.duration,
|
|
1705
|
+
startTime: measure.startTime
|
|
1706
|
+
},
|
|
1707
|
+
timestamp: measure.startTime / 1e3
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Log a trace message using Sentry.logger (if available).
|
|
1714
|
+
* Falls back to captureMessage if logger is not available.
|
|
1715
|
+
*
|
|
1716
|
+
* @param message - Trace message to log
|
|
1717
|
+
* @param attributes - Optional attributes to include with the log
|
|
1718
|
+
*/
|
|
1719
|
+
logTrace(message, attributes) {
|
|
1720
|
+
if (!this.enabled || !this.client) return;
|
|
1721
|
+
if (this.client.logger?.trace) try {
|
|
1722
|
+
this.client.logger.trace(message, attributes);
|
|
1723
|
+
return;
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
logWarn("[Sentry Plugin] Failed to log trace", { error });
|
|
1726
|
+
}
|
|
1727
|
+
this.captureMessage(message, "debug", attributes);
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Log a debug message using Sentry.logger (if available).
|
|
1731
|
+
* Falls back to captureMessage if logger is not available.
|
|
1732
|
+
*
|
|
1733
|
+
* @param message - Debug message to log
|
|
1734
|
+
* @param attributes - Optional attributes to include with the log
|
|
1735
|
+
*/
|
|
1736
|
+
logDebug(message, attributes) {
|
|
1737
|
+
if (!this.enabled || !this.client) return;
|
|
1738
|
+
if (this.client.logger?.debug) try {
|
|
1739
|
+
this.client.logger.debug(message, attributes);
|
|
1740
|
+
return;
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
logWarn("[Sentry Plugin] Failed to log debug", { error });
|
|
1743
|
+
}
|
|
1744
|
+
this.captureMessage(message, "debug", attributes);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Log an info message using Sentry.logger (if available).
|
|
1748
|
+
* Falls back to captureMessage if logger is not available.
|
|
1749
|
+
*
|
|
1750
|
+
* @param message - Info message to log
|
|
1751
|
+
* @param attributes - Optional attributes to include with the log
|
|
1752
|
+
*/
|
|
1753
|
+
logInfo(message, attributes) {
|
|
1754
|
+
if (!this.enabled || !this.client) return;
|
|
1755
|
+
if (this.client.logger?.info) try {
|
|
1756
|
+
this.client.logger.info(message, attributes);
|
|
1757
|
+
return;
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
logWarn("[Sentry Plugin] Failed to log info", { error });
|
|
1760
|
+
}
|
|
1761
|
+
this.captureMessage(message, "info", attributes);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Log a warning message using Sentry.logger (if available).
|
|
1765
|
+
* Falls back to captureMessage if logger is not available.
|
|
1766
|
+
*
|
|
1767
|
+
* @param message - Warning message to log
|
|
1768
|
+
* @param attributes - Optional attributes to include with the log
|
|
1769
|
+
*/
|
|
1770
|
+
logWarn(message, attributes) {
|
|
1771
|
+
if (!this.enabled || !this.client) return;
|
|
1772
|
+
if (this.client.logger?.warn) try {
|
|
1773
|
+
this.client.logger.warn(message, attributes);
|
|
1774
|
+
return;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
logWarn("[Sentry Plugin] Failed to log warn", { error });
|
|
1777
|
+
}
|
|
1778
|
+
this.captureMessage(message, "warning", attributes);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Log an error message using Sentry.logger (if available).
|
|
1782
|
+
* Falls back to captureMessage if logger is not available.
|
|
1783
|
+
*
|
|
1784
|
+
* @param message - Error message to log
|
|
1785
|
+
* @param attributes - Optional attributes to include with the log
|
|
1786
|
+
*/
|
|
1787
|
+
logError(message, attributes) {
|
|
1788
|
+
if (!this.enabled || !this.client) return;
|
|
1789
|
+
if (this.client.logger?.error) try {
|
|
1790
|
+
this.client.logger.error(message, attributes);
|
|
1791
|
+
return;
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
logWarn("[Sentry Plugin] Failed to log error", { error });
|
|
1794
|
+
}
|
|
1795
|
+
this.captureMessage(message, "error", attributes);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Log a fatal message using Sentry.logger (if available).
|
|
1799
|
+
* Falls back to captureMessage if logger is not available.
|
|
1800
|
+
*
|
|
1801
|
+
* @param message - Fatal message to log
|
|
1802
|
+
* @param attributes - Optional attributes to include with the log
|
|
1803
|
+
*/
|
|
1804
|
+
logFatal(message, attributes) {
|
|
1805
|
+
if (!this.enabled || !this.client) return;
|
|
1806
|
+
if (this.client.logger?.fatal) try {
|
|
1807
|
+
this.client.logger.fatal(message, attributes);
|
|
1808
|
+
return;
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
logWarn("[Sentry Plugin] Failed to log fatal", { error });
|
|
1811
|
+
}
|
|
1812
|
+
this.captureMessage(message, "error", attributes);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Get the Sentry.logger instance if available.
|
|
1816
|
+
* This allows direct access to Sentry.logger APIs including fmt.
|
|
1817
|
+
*
|
|
1818
|
+
* @returns Sentry.logger instance or undefined if not available
|
|
1819
|
+
*
|
|
1820
|
+
* @example
|
|
1821
|
+
* ```typescript
|
|
1822
|
+
* const logger = sentryPlugin.getLogger();
|
|
1823
|
+
* if (logger) {
|
|
1824
|
+
* logger.info(logger.fmt`User ${user.id} logged in`);
|
|
1825
|
+
* }
|
|
1826
|
+
* ```
|
|
1827
|
+
*/
|
|
1828
|
+
getLogger() {
|
|
1829
|
+
if (!this.enabled || !this.client) return void 0;
|
|
1830
|
+
return this.client.logger;
|
|
1831
|
+
}
|
|
1832
|
+
async flush(timeout) {
|
|
1833
|
+
if (!this.enabled || !this.client || !this.client.flush) return true;
|
|
1834
|
+
try {
|
|
1835
|
+
return await this.client.flush(timeout);
|
|
1836
|
+
} catch (_error) {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
/**
|
|
1842
|
+
* Factory function to create a Sentry plugin.
|
|
1843
|
+
*
|
|
1844
|
+
* Creates a configured SentryPlugin instance with optional configuration.
|
|
1845
|
+
* Auto-detects the appropriate Sentry package based on the environment.
|
|
1846
|
+
*
|
|
1847
|
+
* @param config - Optional Sentry plugin configuration
|
|
1848
|
+
* @returns SentryPlugin instance
|
|
1849
|
+
*
|
|
1850
|
+
* @example
|
|
1851
|
+
* ```typescript
|
|
1852
|
+
* const sentry = createSentryPlugin({
|
|
1853
|
+
* dsn: 'https://...',
|
|
1854
|
+
* environment: 'production',
|
|
1855
|
+
* tracesSampleRate: 0.1,
|
|
1856
|
+
* });
|
|
1857
|
+
* ```
|
|
1858
|
+
*/
|
|
1859
|
+
const createSentryPlugin = (config) => {
|
|
1860
|
+
return new SentryPlugin(config);
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
//#endregion
|
|
1864
|
+
//#region ../observability/src/client-next.ts
|
|
1865
|
+
/**
|
|
1866
|
+
* @fileoverview Next.js client-specific observability export
|
|
1867
|
+
* Next.js client-specific observability export
|
|
1868
|
+
*/
|
|
1869
|
+
/**
|
|
1870
|
+
* Create auto-configured observability for Next.js client
|
|
1871
|
+
*/
|
|
1872
|
+
async function createClientObservability() {
|
|
1873
|
+
const builder = ObservabilityBuilder.create();
|
|
1874
|
+
const enableConsole = shouldEnableConsole(env$2.NEXT_PUBLIC_NODE_ENV, env$2.NEXT_PUBLIC_OBSERVABILITY_CONSOLE_ENABLED, env$2.NEXT_PUBLIC_OBSERVABILITY_DEBUG);
|
|
1875
|
+
builder.withPlugin(createConsolePlugin({
|
|
1876
|
+
prefix: "[Next.js Client]",
|
|
1877
|
+
enabled: enableConsole
|
|
1878
|
+
}));
|
|
1879
|
+
if (env.NEXT_PUBLIC_SENTRY_DSN) builder.withPlugin(createSentryPlugin({ dsn: env.NEXT_PUBLIC_SENTRY_DSN }));
|
|
1880
|
+
const clientBetterStackToken = env$1.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN ?? env$1.NEXT_PUBLIC_BETTERSTACK_TOKEN ?? env$1.NEXT_PUBLIC_LOGTAIL_TOKEN;
|
|
1881
|
+
if (clientBetterStackToken) builder.withPlugin(createBetterStackPlugin({ sourceToken: clientBetterStackToken }));
|
|
1882
|
+
return builder.build();
|
|
298
1883
|
}
|
|
299
1884
|
|
|
300
1885
|
//#endregion
|
|
@@ -317,7 +1902,6 @@ function resetAnalytics() {
|
|
|
317
1902
|
*
|
|
318
1903
|
* @module @od-oneapp/analytics/client/next/components
|
|
319
1904
|
*/
|
|
320
|
-
let globalAnalytics = null;
|
|
321
1905
|
let logger = null;
|
|
322
1906
|
/**
|
|
323
1907
|
* Analytics provider component for App Router.
|
|
@@ -352,11 +1936,11 @@ let logger = null;
|
|
|
352
1936
|
*/
|
|
353
1937
|
function AnalyticsProvider({ autoPageTracking = true, children, config, pageTrackingOptions }) {
|
|
354
1938
|
useEffect(() => {
|
|
355
|
-
if (!
|
|
1939
|
+
if (!getGlobalAnalytics()) {
|
|
356
1940
|
const initAnalytics = async () => {
|
|
357
1941
|
try {
|
|
358
1942
|
logger ??= await createClientObservability();
|
|
359
|
-
|
|
1943
|
+
setGlobalAnalytics(await createNextJSClientAnalytics(config));
|
|
360
1944
|
} catch (error) {
|
|
361
1945
|
if (logger) logger.captureException(error, { message: "Failed to initialize analytics" });
|
|
362
1946
|
}
|