@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.
- package/README.md +523 -0
- package/dist/client-next.d.mts +20 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +64 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +11 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +47 -0
- package/dist/client.mjs.map +1 -0
- package/dist/env.d.mts +15 -0
- package/dist/env.d.mts.map +1 -0
- package/dist/env.mjs +45 -0
- package/dist/env.mjs.map +1 -0
- package/dist/factory-DkY353r8.mjs +380 -0
- package/dist/factory-DkY353r8.mjs.map +1 -0
- package/dist/hooks-useObservability.d.mts +11 -0
- package/dist/hooks-useObservability.d.mts.map +1 -0
- package/dist/hooks-useObservability.mjs +174 -0
- package/dist/hooks-useObservability.mjs.map +1 -0
- package/dist/index-CpcdzWrF.d.mts +24 -0
- package/dist/index-CpcdzWrF.d.mts.map +1 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +97 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manager-BxQqOPEg.d.mts +33 -0
- package/dist/manager-BxQqOPEg.d.mts.map +1 -0
- package/dist/plugin-Bfq-o3nr.d.mts +60 -0
- package/dist/plugin-Bfq-o3nr.d.mts.map +1 -0
- package/dist/plugin-Bt-ygG1m.d.mts +254 -0
- package/dist/plugin-Bt-ygG1m.d.mts.map +1 -0
- package/dist/plugin-CLFwRERa.mjs +593 -0
- package/dist/plugin-CLFwRERa.mjs.map +1 -0
- package/dist/plugin-CP895lBx.mjs +534 -0
- package/dist/plugin-CP895lBx.mjs.map +1 -0
- package/dist/plugin-CaQxviDs.d.mts +61 -0
- package/dist/plugin-CaQxviDs.d.mts.map +1 -0
- package/dist/plugin-lPdJirTY.mjs +234 -0
- package/dist/plugin-lPdJirTY.mjs.map +1 -0
- package/dist/plugins-betterstack-env.d.mts +29 -0
- package/dist/plugins-betterstack-env.d.mts.map +1 -0
- package/dist/plugins-betterstack-env.mjs +75 -0
- package/dist/plugins-betterstack-env.mjs.map +1 -0
- package/dist/plugins-betterstack.d.mts +4 -0
- package/dist/plugins-betterstack.mjs +4 -0
- package/dist/plugins-console.d.mts +37 -0
- package/dist/plugins-console.d.mts.map +1 -0
- package/dist/plugins-console.mjs +196 -0
- package/dist/plugins-console.mjs.map +1 -0
- package/dist/plugins-sentry-env.d.mts +37 -0
- package/dist/plugins-sentry-env.d.mts.map +1 -0
- package/dist/plugins-sentry-env.mjs +79 -0
- package/dist/plugins-sentry-env.mjs.map +1 -0
- package/dist/plugins-sentry-microfrontend-env.d.mts +49 -0
- package/dist/plugins-sentry-microfrontend-env.d.mts.map +1 -0
- package/dist/plugins-sentry-microfrontend-env.mjs +80 -0
- package/dist/plugins-sentry-microfrontend-env.mjs.map +1 -0
- package/dist/plugins-sentry-microfrontend.d.mts +2 -0
- package/dist/plugins-sentry-microfrontend.mjs +3 -0
- package/dist/plugins-sentry.d.mts +5 -0
- package/dist/plugins-sentry.mjs +6 -0
- package/dist/server-edge.d.mts +15 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +53 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +17 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +64 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +11 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +48 -0
- package/dist/server.mjs.map +1 -0
- package/dist/utils-CuGrTcD6.d.mts +77 -0
- package/dist/utils-CuGrTcD6.d.mts.map +1 -0
- package/env.ts +67 -0
- package/package.json +147 -0
- package/src/client-next.ts +131 -0
- package/src/client.ts +70 -0
- package/src/core/index.ts +15 -0
- package/src/core/manager.ts +361 -0
- package/src/core/plugin.ts +61 -0
- package/src/core/types.ts +151 -0
- package/src/factory/builder.ts +132 -0
- package/src/factory/index.ts +67 -0
- package/src/factory/presets.ts +78 -0
- package/src/hooks/useObservability.ts +206 -0
- package/src/plugins/betterstack/env.ts +101 -0
- package/src/plugins/betterstack/index.ts +15 -0
- package/src/plugins/betterstack/plugin.ts +373 -0
- package/src/plugins/console/index.ts +323 -0
- package/src/plugins/sentry/__tests__/plugin-tracing.test.ts +511 -0
- package/src/plugins/sentry/env.ts +93 -0
- package/src/plugins/sentry/index.ts +28 -0
- package/src/plugins/sentry/plugin.ts +953 -0
- package/src/plugins/sentry/types.ts +252 -0
- package/src/plugins/sentry-microfrontend/env.ts +105 -0
- package/src/plugins/sentry-microfrontend/index.ts +12 -0
- package/src/plugins/sentry-microfrontend/multiplexed-transport.ts +221 -0
- package/src/plugins/sentry-microfrontend/plugin.ts +500 -0
- package/src/plugins/sentry-microfrontend/sentry-types.ts +140 -0
- package/src/plugins/sentry-microfrontend/types.ts +130 -0
- package/src/plugins/sentry-microfrontend/utils.ts +326 -0
- package/src/server-edge.ts +113 -0
- package/src/server-next.ts +114 -0
- package/src/server.ts +71 -0
- 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
|
+
}
|