@od-oneapp/observability 2026.1.1301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +523 -0
  2. package/dist/client-next.d.mts +20 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +64 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client.d.mts +11 -0
  7. package/dist/client.d.mts.map +1 -0
  8. package/dist/client.mjs +47 -0
  9. package/dist/client.mjs.map +1 -0
  10. package/dist/env.d.mts +15 -0
  11. package/dist/env.d.mts.map +1 -0
  12. package/dist/env.mjs +45 -0
  13. package/dist/env.mjs.map +1 -0
  14. package/dist/factory-DkY353r8.mjs +380 -0
  15. package/dist/factory-DkY353r8.mjs.map +1 -0
  16. package/dist/hooks-useObservability.d.mts +11 -0
  17. package/dist/hooks-useObservability.d.mts.map +1 -0
  18. package/dist/hooks-useObservability.mjs +174 -0
  19. package/dist/hooks-useObservability.mjs.map +1 -0
  20. package/dist/index-CpcdzWrF.d.mts +24 -0
  21. package/dist/index-CpcdzWrF.d.mts.map +1 -0
  22. package/dist/index.d.mts +88 -0
  23. package/dist/index.d.mts.map +1 -0
  24. package/dist/index.mjs +97 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/manager-BxQqOPEg.d.mts +33 -0
  27. package/dist/manager-BxQqOPEg.d.mts.map +1 -0
  28. package/dist/plugin-Bfq-o3nr.d.mts +60 -0
  29. package/dist/plugin-Bfq-o3nr.d.mts.map +1 -0
  30. package/dist/plugin-Bt-ygG1m.d.mts +254 -0
  31. package/dist/plugin-Bt-ygG1m.d.mts.map +1 -0
  32. package/dist/plugin-CLFwRERa.mjs +593 -0
  33. package/dist/plugin-CLFwRERa.mjs.map +1 -0
  34. package/dist/plugin-CP895lBx.mjs +534 -0
  35. package/dist/plugin-CP895lBx.mjs.map +1 -0
  36. package/dist/plugin-CaQxviDs.d.mts +61 -0
  37. package/dist/plugin-CaQxviDs.d.mts.map +1 -0
  38. package/dist/plugin-lPdJirTY.mjs +234 -0
  39. package/dist/plugin-lPdJirTY.mjs.map +1 -0
  40. package/dist/plugins-betterstack-env.d.mts +29 -0
  41. package/dist/plugins-betterstack-env.d.mts.map +1 -0
  42. package/dist/plugins-betterstack-env.mjs +75 -0
  43. package/dist/plugins-betterstack-env.mjs.map +1 -0
  44. package/dist/plugins-betterstack.d.mts +4 -0
  45. package/dist/plugins-betterstack.mjs +4 -0
  46. package/dist/plugins-console.d.mts +37 -0
  47. package/dist/plugins-console.d.mts.map +1 -0
  48. package/dist/plugins-console.mjs +196 -0
  49. package/dist/plugins-console.mjs.map +1 -0
  50. package/dist/plugins-sentry-env.d.mts +37 -0
  51. package/dist/plugins-sentry-env.d.mts.map +1 -0
  52. package/dist/plugins-sentry-env.mjs +79 -0
  53. package/dist/plugins-sentry-env.mjs.map +1 -0
  54. package/dist/plugins-sentry-microfrontend-env.d.mts +49 -0
  55. package/dist/plugins-sentry-microfrontend-env.d.mts.map +1 -0
  56. package/dist/plugins-sentry-microfrontend-env.mjs +80 -0
  57. package/dist/plugins-sentry-microfrontend-env.mjs.map +1 -0
  58. package/dist/plugins-sentry-microfrontend.d.mts +2 -0
  59. package/dist/plugins-sentry-microfrontend.mjs +3 -0
  60. package/dist/plugins-sentry.d.mts +5 -0
  61. package/dist/plugins-sentry.mjs +6 -0
  62. package/dist/server-edge.d.mts +15 -0
  63. package/dist/server-edge.d.mts.map +1 -0
  64. package/dist/server-edge.mjs +53 -0
  65. package/dist/server-edge.mjs.map +1 -0
  66. package/dist/server-next.d.mts +17 -0
  67. package/dist/server-next.d.mts.map +1 -0
  68. package/dist/server-next.mjs +64 -0
  69. package/dist/server-next.mjs.map +1 -0
  70. package/dist/server.d.mts +11 -0
  71. package/dist/server.d.mts.map +1 -0
  72. package/dist/server.mjs +48 -0
  73. package/dist/server.mjs.map +1 -0
  74. package/dist/utils-CuGrTcD6.d.mts +77 -0
  75. package/dist/utils-CuGrTcD6.d.mts.map +1 -0
  76. package/env.ts +67 -0
  77. package/package.json +147 -0
  78. package/src/client-next.ts +131 -0
  79. package/src/client.ts +70 -0
  80. package/src/core/index.ts +15 -0
  81. package/src/core/manager.ts +361 -0
  82. package/src/core/plugin.ts +61 -0
  83. package/src/core/types.ts +151 -0
  84. package/src/factory/builder.ts +132 -0
  85. package/src/factory/index.ts +67 -0
  86. package/src/factory/presets.ts +78 -0
  87. package/src/hooks/useObservability.ts +206 -0
  88. package/src/plugins/betterstack/env.ts +101 -0
  89. package/src/plugins/betterstack/index.ts +15 -0
  90. package/src/plugins/betterstack/plugin.ts +373 -0
  91. package/src/plugins/console/index.ts +323 -0
  92. package/src/plugins/sentry/__tests__/plugin-tracing.test.ts +511 -0
  93. package/src/plugins/sentry/env.ts +93 -0
  94. package/src/plugins/sentry/index.ts +28 -0
  95. package/src/plugins/sentry/plugin.ts +953 -0
  96. package/src/plugins/sentry/types.ts +252 -0
  97. package/src/plugins/sentry-microfrontend/env.ts +105 -0
  98. package/src/plugins/sentry-microfrontend/index.ts +12 -0
  99. package/src/plugins/sentry-microfrontend/multiplexed-transport.ts +221 -0
  100. package/src/plugins/sentry-microfrontend/plugin.ts +500 -0
  101. package/src/plugins/sentry-microfrontend/sentry-types.ts +140 -0
  102. package/src/plugins/sentry-microfrontend/types.ts +130 -0
  103. package/src/plugins/sentry-microfrontend/utils.ts +326 -0
  104. package/src/server-edge.ts +113 -0
  105. package/src/server-next.ts +114 -0
  106. package/src/server.ts +71 -0
  107. package/src/shared.ts +148 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * @fileoverview ObservabilityManager - Core orchestrator for multiple observability providers
3
+ * ObservabilityManager - Core orchestrator for multiple observability providers
4
+ */
5
+
6
+ import { logError, logWarn } from '@repo/shared/logs';
7
+
8
+ import type { ObservabilityPlugin, ObservabilityServerPlugin, PluginLifecycle } from './plugin';
9
+ import type {
10
+ Breadcrumb,
11
+ LogLevel,
12
+ ObservabilityContext,
13
+ ObservabilityScope,
14
+ ObservabilityServer,
15
+ ObservabilityUser,
16
+ } from './types';
17
+
18
+ /**
19
+ * Manager that orchestrates multiple observability plugins
20
+ * Broadcasts all method calls to enabled plugins
21
+ */
22
+ export class ObservabilityManager implements ObservabilityServer {
23
+ private plugins = new Map<string, ObservabilityPlugin | ObservabilityServerPlugin>();
24
+ private initialized = false;
25
+ private initializationPromise: Promise<void> | null = null;
26
+ private lifecycle: PluginLifecycle = {};
27
+ private initializationError: Error | null = null;
28
+
29
+ /**
30
+ * Create a new ObservabilityManager instance.
31
+ *
32
+ * @param lifecycle - Optional lifecycle callbacks for plugin events
33
+ */
34
+ constructor(lifecycle?: PluginLifecycle) {
35
+ if (lifecycle) {
36
+ this.lifecycle = lifecycle;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Add a plugin to the manager
42
+ */
43
+ addPlugin(plugin: ObservabilityPlugin | ObservabilityServerPlugin): this {
44
+ this.plugins.set(plugin.name, plugin);
45
+ return this;
46
+ }
47
+
48
+ /**
49
+ * Get a specific plugin by name.
50
+ *
51
+ * @param name - Name of the plugin to retrieve
52
+ * @returns Plugin instance if found, undefined otherwise
53
+ */
54
+ getPlugin<T extends ObservabilityPlugin>(name: string): T | undefined {
55
+ return this.plugins.get(name) as T;
56
+ }
57
+
58
+ /**
59
+ * Get all registered plugins.
60
+ *
61
+ * @returns Array of all registered plugins
62
+ */
63
+ getPlugins(): ObservabilityPlugin[] {
64
+ return Array.from(this.plugins.values());
65
+ }
66
+
67
+ /**
68
+ * List all registered plugins (alias for getPlugins).
69
+ *
70
+ * @returns Array of all registered plugins
71
+ */
72
+ listPlugins(): ObservabilityPlugin[] {
73
+ return this.getPlugins();
74
+ }
75
+
76
+ /**
77
+ * Initialize all plugins
78
+ * Note: Failed initialization is retryable - subsequent calls will re-attempt initialization.
79
+ * This allows recovery from transient failures (network issues, temporary misconfigurations).
80
+ */
81
+ async initialize(): Promise<void> {
82
+ // If there's already an initialization in progress, wait for it
83
+ if (this.initializationPromise) {
84
+ return this.initializationPromise;
85
+ }
86
+
87
+ // If already initialized successfully, return early
88
+ if (this.initialized) {
89
+ return;
90
+ }
91
+
92
+ // Allow retry if previous attempt failed
93
+ if (this.initializationError) {
94
+ // Log retry attempt for debugging
95
+ logWarn('ObservabilityManager: Retrying failed initialization');
96
+ // Clear previous error to allow fresh attempt
97
+ this.initializationError = null;
98
+ }
99
+
100
+ // Create the initialization promise and store it to prevent concurrent calls
101
+ this.initializationPromise = this.doInitialize();
102
+
103
+ try {
104
+ await this.initializationPromise;
105
+ } finally {
106
+ // Clear the promise after initialization completes (success or failure)
107
+ this.initializationPromise = null;
108
+ }
109
+ }
110
+
111
+ private async doInitialize(): Promise<void> {
112
+ const initPromises = Array.from(this.plugins.values())
113
+ .filter(plugin => plugin.enabled && plugin.initialize)
114
+ .map(async plugin => {
115
+ try {
116
+ if (plugin.initialize) {
117
+ await plugin.initialize();
118
+ }
119
+ this.lifecycle.onInitialized?.(plugin);
120
+ } catch (error) {
121
+ logError(`Failed to initialize plugin ${plugin.name}`, {
122
+ error,
123
+ pluginName: plugin.name,
124
+ });
125
+ this.lifecycle.onError?.(error as Error, plugin);
126
+ // Re-throw to propagate error to Promise.all
127
+ throw error;
128
+ }
129
+ });
130
+
131
+ try {
132
+ await Promise.all(initPromises);
133
+ this.initialized = true;
134
+ } catch (error) {
135
+ this.initializationError = error instanceof Error ? error : new Error(String(error));
136
+ throw this.initializationError;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Check if initialization had an error
142
+ * @returns `true` if initialization failed, `false` otherwise
143
+ */
144
+ hasInitializationError(): boolean {
145
+ return this.initializationError !== null;
146
+ }
147
+
148
+ /**
149
+ * Get the initialization error if one occurred
150
+ * @returns The initialization error or `null` if no error
151
+ */
152
+ getInitializationError(): Error | null {
153
+ return this.initializationError;
154
+ }
155
+
156
+ /**
157
+ * Shutdown all plugins
158
+ */
159
+ async shutdown(): Promise<void> {
160
+ const shutdownPromises = Array.from(this.plugins.values())
161
+ .filter(plugin => plugin.shutdown)
162
+ .map(async plugin => {
163
+ try {
164
+ if (plugin.shutdown) {
165
+ await plugin.shutdown();
166
+ }
167
+ this.lifecycle.onShutdown?.(plugin);
168
+ } catch (error) {
169
+ logError(`Failed to shutdown plugin ${plugin.name}`, { error, pluginName: plugin.name });
170
+ this.lifecycle.onError?.(error as Error, plugin);
171
+ }
172
+ });
173
+
174
+ await Promise.allSettled(shutdownPromises);
175
+ this.initialized = false;
176
+ }
177
+
178
+ /**
179
+ * Broadcast exception to all enabled plugins.
180
+ *
181
+ * @param error - Error object or unknown error value
182
+ * @param context - Optional additional context data
183
+ */
184
+ captureException(error: Error | unknown, context?: ObservabilityContext): void {
185
+ this.broadcast(plugin => plugin.captureException(error, context));
186
+ }
187
+
188
+ /**
189
+ * Broadcast message to all enabled plugins.
190
+ *
191
+ * @param message - Message to log
192
+ * @param level - Log level (default: 'info')
193
+ * @param context - Optional additional context data
194
+ */
195
+ captureMessage(message: string, level: LogLevel = 'info', context?: ObservabilityContext): void {
196
+ this.broadcast(plugin => plugin.captureMessage(message, level, context));
197
+ }
198
+
199
+ /**
200
+ * Validate and sanitize user data to prevent injection and DoS attacks
201
+ * @param user - User data to validate
202
+ * @returns Validated user data with length limits and format validation applied
203
+ */
204
+ private validateUser(user: ObservabilityUser | null): ObservabilityUser | null {
205
+ if (!user) return null;
206
+
207
+ // Validate required ID field
208
+ const id = String(user.id).trim().slice(0, 255);
209
+ if (!id) {
210
+ // Log warning but don't throw - graceful degradation
211
+ logWarn('ObservabilityManager: User ID is empty, ignoring user data');
212
+ return null;
213
+ }
214
+
215
+ const validated: ObservabilityUser = { id };
216
+
217
+ // Validate email format if provided
218
+ if ('email' in user && user.email) {
219
+ const email = String(user.email).trim().slice(0, 255);
220
+ // More robust email validation - RFC 5322 simplified but stricter
221
+ // Allows: alphanumeric, dots, plus, hyphens, underscores before @
222
+ // Requires: valid domain with at least one dot after @
223
+ if (
224
+ email &&
225
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded input (max 255 chars), RFC 5322 compliant pattern
226
+ /^[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(
227
+ email,
228
+ )
229
+ ) {
230
+ validated.email = email;
231
+ }
232
+ }
233
+
234
+ // Validate username if provided
235
+ if ('username' in user && user.username) {
236
+ const username = String(user.username).trim().slice(0, 255);
237
+ if (username) {
238
+ validated.username = username;
239
+ }
240
+ }
241
+
242
+ // Validate IP address format if provided
243
+ if ('ip_address' in user && user.ip_address) {
244
+ const ip = String(user.ip_address).trim();
245
+ // Stricter IP validation
246
+ // IPv4: Each octet must be 0-255
247
+ const isValidIPv4 =
248
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded pattern for IPv4 validation, input is trimmed string
249
+ /^(?:(?: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(
250
+ ip,
251
+ );
252
+ // IPv6: Basic format check (full validation is complex, this catches most invalid cases)
253
+ const looksLikeIPv6 =
254
+ /^[0-9a-fA-F:]+$/.test(ip) && ip.includes(':') && ip.split(':').length <= 8;
255
+
256
+ if (isValidIPv4 || looksLikeIPv6) {
257
+ validated.ip_address = ip.slice(0, 45); // IPv6 max length
258
+ }
259
+ }
260
+
261
+ return validated;
262
+ }
263
+
264
+ /**
265
+ * Set user on all enabled plugins
266
+ * User data is validated and sanitized before being set
267
+ * @param user - User data to set (will be validated)
268
+ */
269
+ setUser(user: ObservabilityUser | null): void {
270
+ const validatedUser = this.validateUser(user);
271
+ this.broadcast(plugin => plugin.setUser(validatedUser));
272
+ }
273
+
274
+ /**
275
+ * Add breadcrumb to all enabled plugins.
276
+ *
277
+ * @param breadcrumb - Breadcrumb data to add
278
+ */
279
+ addBreadcrumb(breadcrumb: Breadcrumb): void {
280
+ this.broadcast(plugin => plugin.addBreadcrumb(breadcrumb));
281
+ }
282
+
283
+ /**
284
+ * Execute callback within scope for all enabled plugins.
285
+ *
286
+ * @param callback - Callback that receives the scope
287
+ */
288
+ withScope(callback: (scope: ObservabilityScope) => void): void {
289
+ this.broadcast(plugin => plugin.withScope(callback));
290
+ }
291
+
292
+ /**
293
+ * Flush all plugins that support it.
294
+ *
295
+ * Waits for all enabled plugins with flush capability to send pending events.
296
+ *
297
+ * @param timeout - Maximum time to wait in milliseconds
298
+ * @returns Promise resolving to true if all plugins flushed successfully
299
+ */
300
+ async flush(timeout?: number): Promise<boolean> {
301
+ const flushPromises = Array.from(this.plugins.values())
302
+ .filter((plugin): plugin is ObservabilityServerPlugin => {
303
+ return plugin.enabled && 'flush' in plugin && typeof plugin.flush === 'function';
304
+ })
305
+ .map(plugin => plugin.flush(timeout));
306
+
307
+ if (flushPromises.length === 0) {
308
+ return true;
309
+ }
310
+
311
+ const results = await Promise.allSettled(flushPromises);
312
+ return results.every(result => result.status === 'fulfilled' && result.value === true);
313
+ }
314
+
315
+ /**
316
+ * Helper to broadcast a method call to all enabled plugins
317
+ * Uses nested try-catch to ensure errors in error handling don't prevent other plugins from executing
318
+ */
319
+ private broadcast(fn: (plugin: ObservabilityPlugin) => void): void {
320
+ this.plugins.forEach(plugin => {
321
+ if (plugin.enabled) {
322
+ try {
323
+ fn(plugin);
324
+ } catch (error) {
325
+ // Safely handle error without throwing - nested try-catch ensures resilience
326
+ try {
327
+ logError(`Plugin ${plugin.name} error`, { error, pluginName: plugin.name });
328
+ } catch {
329
+ // Logger unavailable - continue silently
330
+ }
331
+
332
+ try {
333
+ this.lifecycle.onError?.(error as Error, plugin);
334
+ } catch {
335
+ // Error handler failed - continue to next plugin
336
+ }
337
+ }
338
+ }
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Check if manager has any enabled plugins.
344
+ *
345
+ * @returns True if at least one plugin is enabled, false otherwise
346
+ */
347
+ hasEnabledPlugins(): boolean {
348
+ return Array.from(this.plugins.values()).some(plugin => plugin.enabled);
349
+ }
350
+
351
+ /**
352
+ * Get names of all enabled plugins.
353
+ *
354
+ * @returns Array of plugin names that are currently enabled
355
+ */
356
+ getEnabledPluginNames(): string[] {
357
+ return Array.from(this.plugins.values())
358
+ .filter(plugin => plugin.enabled)
359
+ .map(plugin => plugin.name);
360
+ }
361
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @fileoverview Core plugin interface for observability providers
3
+ * Core plugin interface for observability providers
4
+ */
5
+
6
+ import type { ObservabilityClient, ObservabilityServer } from './types';
7
+
8
+ /**
9
+ * Base plugin interface that all observability providers must implement
10
+ */
11
+ export interface ObservabilityPlugin<TClient = any> extends ObservabilityClient {
12
+ /**
13
+ * Unique name for this plugin
14
+ */
15
+ name: string;
16
+
17
+ /**
18
+ * Whether this plugin is currently enabled
19
+ */
20
+ enabled: boolean;
21
+
22
+ /**
23
+ * Get the native client instance (e.g., Sentry SDK)
24
+ * This allows direct access to provider-specific features
25
+ */
26
+ getClient(): TClient | undefined;
27
+
28
+ /**
29
+ * Initialize the plugin with optional configuration
30
+ */
31
+ initialize?(config?: any): Promise<void>;
32
+
33
+ /**
34
+ * Cleanup resources when shutting down
35
+ */
36
+ shutdown?(): Promise<void>;
37
+ }
38
+
39
+ /**
40
+ * Server-side plugin interface with additional flush capability
41
+ */
42
+ export interface ObservabilityServerPlugin<TClient = any>
43
+ extends ObservabilityPlugin<TClient>, ObservabilityServer {
44
+ // Inherits flush() from ObservabilityServer
45
+ }
46
+
47
+ /**
48
+ * Factory function type for creating plugins
49
+ */
50
+ export type PluginFactory<TConfig = any, TPlugin = ObservabilityPlugin> = (
51
+ config?: TConfig,
52
+ ) => TPlugin;
53
+
54
+ /**
55
+ * Plugin lifecycle events
56
+ */
57
+ export interface PluginLifecycle {
58
+ onError?: (error: Error, plugin: ObservabilityPlugin) => void;
59
+ onInitialized?: (plugin: ObservabilityPlugin) => void;
60
+ onShutdown?: (plugin: ObservabilityPlugin) => void;
61
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @fileoverview Core types for the observability package
3
+ * Core types for the observability package
4
+ * These types are provider-agnostic and define the common interface
5
+ */
6
+
7
+ /**
8
+ * User information for observability context.
9
+ *
10
+ * Represents a user associated with observability events. All fields are validated
11
+ * and sanitized by ObservabilityManager to prevent injection attacks.
12
+ */
13
+ export interface ObservabilityUser {
14
+ /** Unique identifier for the user */
15
+ id: string;
16
+ /** User's email address (validated format) */
17
+ email?: string;
18
+ /** User's username */
19
+ username?: string;
20
+ /** User's IP address (IPv4 or IPv6, validated format) */
21
+ ip_address?: string;
22
+ /**
23
+ * @deprecated Additional custom fields are deprecated and will be removed in a future version.
24
+ * Custom fields are stripped by validateUser() for security.
25
+ * Only the defined fields above are validated and sanitized by the ObservabilityManager.
26
+ */
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /**
31
+ * Additional context data for observability events.
32
+ *
33
+ * Can contain any key-value pairs to provide additional information about
34
+ * an event. Values are serialized when sent to observability providers.
35
+ */
36
+ export interface ObservabilityContext {
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ /**
41
+ * Scope for isolating observability context.
42
+ *
43
+ * Represents a provider-agnostic scope that can be used to set
44
+ * context-specific tags, user info, and other metadata.
45
+ */
46
+ export interface ObservabilityScope {
47
+ setTag(key: string, value: string): void;
48
+ setExtra(key: string, value: unknown): void;
49
+ setUser(user: ObservabilityUser | null): void;
50
+ setLevel(level: LogLevel): void;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ /**
55
+ * Log level for observability messages.
56
+ *
57
+ * Ordered from most verbose (debug) to least verbose (error).
58
+ */
59
+ export type LogLevel = 'debug' | 'info' | 'warning' | 'error';
60
+
61
+ /**
62
+ * Breadcrumb for tracking user actions and events.
63
+ *
64
+ * Breadcrumbs provide a trail of events leading up to an error or important event,
65
+ * helping with debugging and understanding user flow.
66
+ */
67
+ export interface Breadcrumb {
68
+ /** The message describing the breadcrumb */
69
+ message: string;
70
+ /** Category of the breadcrumb (e.g., 'navigation', 'user', 'http') */
71
+ category?: string;
72
+ /** Severity level of the breadcrumb */
73
+ level?: LogLevel | 'critical';
74
+ /** Additional data associated with the breadcrumb */
75
+ data?: Record<string, unknown>;
76
+ /** Timestamp when the breadcrumb occurred (Unix timestamp in milliseconds) */
77
+ timestamp?: number;
78
+ }
79
+
80
+ /**
81
+ * Client-side observability interface.
82
+ *
83
+ * Defines the core methods available in all environments (browser, server, edge).
84
+ * All methods are synchronous and fire-and-forget.
85
+ */
86
+ export interface ObservabilityClient {
87
+ /**
88
+ * Capture an exception/error.
89
+ *
90
+ * @param error - Error object or unknown error value
91
+ * @param context - Optional additional context data
92
+ */
93
+ captureException(error: Error | unknown, context?: ObservabilityContext): void;
94
+ /**
95
+ * Capture a log message.
96
+ *
97
+ * @param message - Message to log
98
+ * @param level - Log level (default: 'info')
99
+ * @param context - Optional additional context data
100
+ */
101
+ captureMessage(message: string, level?: LogLevel, context?: ObservabilityContext): void;
102
+ /**
103
+ * Set the current user context.
104
+ *
105
+ * @param user - User information, or null to clear
106
+ */
107
+ setUser(user: ObservabilityUser | null): void;
108
+ /**
109
+ * Add a breadcrumb to track user actions.
110
+ *
111
+ * @param breadcrumb - Breadcrumb data
112
+ */
113
+ addBreadcrumb(breadcrumb: Breadcrumb): void;
114
+ /**
115
+ * Execute a callback within a new scope.
116
+ *
117
+ * @param callback - Callback that receives the scope
118
+ */
119
+ withScope(callback: (scope: ObservabilityScope) => void): void;
120
+ }
121
+
122
+ /**
123
+ * Server-side observability interface.
124
+ *
125
+ * Extends ObservabilityClient with additional server-specific capabilities
126
+ * like flushing pending events before shutdown.
127
+ */
128
+ export interface ObservabilityServer extends ObservabilityClient {
129
+ /**
130
+ * Flush pending events to providers.
131
+ *
132
+ * Waits for all pending events to be sent before resolving. Useful before
133
+ * application shutdown to ensure all events are delivered.
134
+ *
135
+ * @param timeout - Maximum time to wait in milliseconds
136
+ * @returns Promise resolving to true if all events flushed, false otherwise
137
+ */
138
+ flush(timeout?: number): Promise<boolean>;
139
+ }
140
+
141
+ /**
142
+ * Configuration for observability system.
143
+ */
144
+ export interface ObservabilityConfig {
145
+ /** Whether observability is enabled */
146
+ enabled?: boolean;
147
+ /** Environment name */
148
+ environment?: 'development' | 'preview' | 'production';
149
+ /** Enable debug logging */
150
+ debug?: boolean;
151
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @fileoverview ObservabilityBuilder - Fluent API for building observability instances
3
+ * ObservabilityBuilder - Fluent API for building observability instances
4
+ */
5
+
6
+ import { logError } from '@repo/shared/logs';
7
+
8
+ import { ObservabilityManager } from '../core/manager';
9
+
10
+ import type {
11
+ ObservabilityPlugin,
12
+ ObservabilityServerPlugin,
13
+ PluginLifecycle,
14
+ } from '../core/plugin';
15
+
16
+ /**
17
+ * Builder for creating configured ObservabilityManager instances
18
+ */
19
+ export class ObservabilityBuilder {
20
+ private plugins: (ObservabilityPlugin | ObservabilityServerPlugin)[] = [];
21
+ private lifecycle: PluginLifecycle = {};
22
+ private autoInitialize = true;
23
+
24
+ /**
25
+ * Add a plugin to the observability stack
26
+ * @param plugin - Observability plugin to add
27
+ * @returns Builder instance for chaining
28
+ */
29
+ withPlugin(plugin: ObservabilityPlugin | ObservabilityServerPlugin): this {
30
+ if (plugin) {
31
+ this.plugins.push(plugin);
32
+ }
33
+ return this;
34
+ }
35
+
36
+ /**
37
+ * Add multiple plugins at once
38
+ * @param plugins - Array of observability plugins to add
39
+ * @returns Builder instance for chaining
40
+ */
41
+ withPlugins(plugins: (ObservabilityPlugin | ObservabilityServerPlugin)[]): this {
42
+ if (plugins && Array.isArray(plugins)) {
43
+ const validPlugins = plugins.filter(plugin => plugin != null);
44
+ this.plugins.push(...validPlugins);
45
+ }
46
+ return this;
47
+ }
48
+
49
+ /**
50
+ * Set lifecycle callbacks for plugin management
51
+ * @param lifecycle - Lifecycle callback configuration
52
+ * @returns Builder instance for chaining
53
+ */
54
+ withLifecycle(lifecycle: PluginLifecycle): this {
55
+ this.lifecycle = { ...this.lifecycle, ...lifecycle };
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Configure whether to auto-initialize plugins (default: true)
61
+ */
62
+ withAutoInitialize(autoInitialize: boolean): this {
63
+ this.autoInitialize = autoInitialize;
64
+ return this;
65
+ }
66
+
67
+ /**
68
+ * Build the ObservabilityManager instance
69
+ * @returns Configured ObservabilityManager instance
70
+ */
71
+ build(): ObservabilityManager {
72
+ const manager = new ObservabilityManager(this.lifecycle);
73
+
74
+ // Add all plugins
75
+ this.plugins.forEach(plugin => manager.addPlugin(plugin));
76
+
77
+ // Auto-initialize if enabled and not in edge runtime
78
+ if (
79
+ this.autoInitialize &&
80
+ typeof process !== 'undefined' &&
81
+ process.env.NEXT_RUNTIME !== 'edge'
82
+ ) {
83
+ // Initialize asynchronously with safe error handling
84
+ void (async () => {
85
+ try {
86
+ await manager.initialize();
87
+ } catch (error) {
88
+ // Store error in manager for later inspection
89
+ // Note: We can't directly set private property, but manager stores it internally
90
+
91
+ // Try logError, but don't fail if unavailable
92
+ try {
93
+ logError('Failed to initialize observability', { error });
94
+ } catch {
95
+ // Logger unavailable - error is stored in manager via initialize() method
96
+ }
97
+ }
98
+ })();
99
+ }
100
+
101
+ return manager;
102
+ }
103
+
104
+ /**
105
+ * Build and initialize the ObservabilityManager instance
106
+ * @returns Promise resolving to initialized ObservabilityManager
107
+ */
108
+ async buildWithAutoInit(): Promise<ObservabilityManager> {
109
+ const manager = new ObservabilityManager(this.lifecycle);
110
+
111
+ // Add all plugins
112
+ this.plugins.forEach(plugin => manager.addPlugin(plugin));
113
+
114
+ // Initialize all plugins - catch errors but still return manager (graceful degradation)
115
+ try {
116
+ await manager.initialize();
117
+ } catch {
118
+ // Error is stored in manager, but we still return it for graceful degradation
119
+ // This allows the application to continue even if observability initialization fails
120
+ }
121
+
122
+ return manager;
123
+ }
124
+
125
+ /**
126
+ * Create a new builder instance
127
+ * @returns New ObservabilityBuilder instance
128
+ */
129
+ static create(): ObservabilityBuilder {
130
+ return new ObservabilityBuilder();
131
+ }
132
+ }