@bugwatch/fastify 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +22 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +26 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -167,14 +167,14 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
167
167
|
...options
|
|
168
168
|
};
|
|
169
169
|
const requestStartTimes = /* @__PURE__ */ new WeakMap();
|
|
170
|
-
const requestContexts = /* @__PURE__ */ new WeakMap();
|
|
171
170
|
fastify.decorateRequest("bugwatch", null);
|
|
172
|
-
fastify.addHook("onRequest",
|
|
171
|
+
fastify.addHook("onRequest", (request, _reply, done) => {
|
|
173
172
|
const client = (0, import_core.getClient)();
|
|
174
|
-
if (!client)
|
|
173
|
+
if (!client) {
|
|
174
|
+
return done();
|
|
175
|
+
}
|
|
175
176
|
requestStartTimes.set(request, Date.now());
|
|
176
177
|
const scopedContext = (0, import_core.createScopedContext)();
|
|
177
|
-
requestContexts.set(request, scopedContext);
|
|
178
178
|
request.bugwatch = createBugwatchDecorator(request, opts);
|
|
179
179
|
if (opts.extractUser) {
|
|
180
180
|
const user = opts.extractUser(request);
|
|
@@ -182,17 +182,20 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
182
182
|
scopedContext.user = user;
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
(0, import_core.runWithContext)(scopedContext, () => {
|
|
186
|
+
if (opts.addBreadcrumbs) {
|
|
187
|
+
(0, import_core.addBreadcrumb)({
|
|
188
|
+
category: "http",
|
|
189
|
+
message: `${request.method} ${request.url}`,
|
|
190
|
+
level: "info",
|
|
191
|
+
data: {
|
|
192
|
+
method: request.method,
|
|
193
|
+
url: request.url
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
done();
|
|
198
|
+
});
|
|
196
199
|
});
|
|
197
200
|
fastify.addHook("onResponse", async (request, reply) => {
|
|
198
201
|
if (!opts.addBreadcrumbs) return;
|
|
@@ -212,7 +215,7 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
212
215
|
}
|
|
213
216
|
});
|
|
214
217
|
});
|
|
215
|
-
fastify.addHook("onError", async (request,
|
|
218
|
+
fastify.addHook("onError", async (request, _reply, error) => {
|
|
216
219
|
if (!opts.captureErrors) return;
|
|
217
220
|
const client = (0, import_core.getClient)();
|
|
218
221
|
if (!client) return;
|
|
@@ -221,6 +224,9 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
221
224
|
await (0, import_core.flush)();
|
|
222
225
|
}
|
|
223
226
|
});
|
|
227
|
+
fastify.addHook("onClose", async () => {
|
|
228
|
+
await (0, import_core.close)();
|
|
229
|
+
});
|
|
224
230
|
};
|
|
225
231
|
var bugwatchPlugin = (0, import_fastify_plugin.default)(bugwatchPluginImpl, {
|
|
226
232
|
fastify: "4.x || 5.x",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/setup.ts","../src/plugin.ts"],"sourcesContent":["/**\n * @bugwatch/fastify - Fastify integration for Bugwatch\n *\n * This package provides a plugin for automatically capturing errors\n * and request context in Fastify applications.\n *\n * @example One-liner setup (recommended)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Single call handles everything (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n *\n * @example Manual setup (for more control)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n * await fastify.register(bugwatchPlugin);\n *\n * // request.bugwatch is available for manual capture\n * fastify.get(\"/risky\", async (request) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\n\n// Export one-liner setup\nexport { setup } from \"./setup\";\nexport type { BugwatchFastifySetupOptions } from \"./setup\";\n\n// Export plugin\nexport { bugwatchPlugin } from \"./plugin\";\n\n// Export types\nexport type {\n BugwatchFastifyOptions,\n BugwatchDecorator,\n} from \"./types\";\n\n// Re-export core functions for convenience\nexport {\n init,\n getClient,\n captureException,\n captureMessage,\n addBreadcrumb,\n setUser,\n setTag,\n setExtra,\n flush,\n close,\n} from \"@bugwatch/core\";\n","/**\n * One-liner setup for Bugwatch Fastify integration.\n *\n * This module provides a simplified setup function that handles all\n * initialization and plugin registration in a single call.\n */\n\nimport type { FastifyInstance } from \"fastify\";\nimport { init, getClient, type BugwatchOptions } from \"@bugwatch/core\";\nimport { bugwatchPlugin } from \"./plugin\";\nimport type { BugwatchFastifyOptions } from \"./types\";\n\n/**\n * Combined options for Fastify setup.\n * Includes both core SDK options and Fastify-specific plugin options.\n */\nexport interface BugwatchFastifySetupOptions\n extends BugwatchFastifyOptions,\n Partial<BugwatchOptions> {}\n\n/**\n * Set up Bugwatch for a Fastify application with a single call.\n *\n * This function:\n * 1. Initializes the Bugwatch SDK (using env vars if no apiKey provided)\n * 2. Registers the Bugwatch plugin with the Fastify instance\n *\n * @param fastify - The Fastify instance\n * @param options - Configuration options (optional if BUGWATCH_API_KEY env var is set)\n * @returns Promise that resolves when the plugin is registered\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Minimal setup (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * environment: \"production\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport async function setup(\n fastify: FastifyInstance,\n options?: BugwatchFastifySetupOptions\n): Promise<void> {\n // Initialize core SDK if not already initialized\n if (!getClient()) {\n // Extract core options\n const coreOptions: Partial<BugwatchOptions> = {};\n if (options?.apiKey) coreOptions.apiKey = options.apiKey;\n if (options?.endpoint) coreOptions.endpoint = options.endpoint;\n if (options?.environment) coreOptions.environment = options.environment;\n if (options?.release) coreOptions.release = options.release;\n if (options?.debug !== undefined) coreOptions.debug = options.debug;\n if (options?.sampleRate !== undefined) coreOptions.sampleRate = options.sampleRate;\n if (options?.maxBreadcrumbs !== undefined) coreOptions.maxBreadcrumbs = options.maxBreadcrumbs;\n if (options?.tags) coreOptions.tags = options.tags;\n if (options?.user) coreOptions.user = options.user;\n if (options?.beforeSend) coreOptions.beforeSend = options.beforeSend;\n if (options?.ignoreErrors) coreOptions.ignoreErrors = options.ignoreErrors;\n\n init(coreOptions);\n }\n\n // Extract plugin-specific options\n const pluginOptions: BugwatchFastifyOptions = {};\n if (options?.extractUser) pluginOptions.extractUser = options.extractUser;\n if (options?.filterHeaders) pluginOptions.filterHeaders = options.filterHeaders;\n if (options?.filterBody) pluginOptions.filterBody = options.filterBody;\n if (options?.includeBody !== undefined) pluginOptions.includeBody = options.includeBody;\n if (options?.addBreadcrumbs !== undefined) pluginOptions.addBreadcrumbs = options.addBreadcrumbs;\n if (options?.captureErrors !== undefined) pluginOptions.captureErrors = options.captureErrors;\n if (options?.flushOnError !== undefined) pluginOptions.flushOnError = options.flushOnError;\n\n // Register the plugin\n await fastify.register(bugwatchPlugin, pluginOptions);\n}\n","import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport {\n getClient,\n captureException,\n captureMessage as coreCaptureMessage,\n addBreadcrumb,\n setUser,\n flush,\n runWithContextAsync,\n createScopedContext,\n type RequestContext,\n type BugwatchClient,\n type ScopedContext,\n} from \"@bugwatch/core\";\nimport type { BugwatchFastifyOptions, BugwatchDecorator } from \"./types\";\n\n/**\n * Headers that should be filtered out by default for security.\n */\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n]);\n\n/**\n * Body fields that should be filtered out by default for security.\n */\nconst SENSITIVE_BODY_FIELDS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"credit_card\",\n \"creditCard\",\n \"ssn\",\n \"social_security\",\n]);\n\n/**\n * Default header filter function.\n */\nfunction defaultHeaderFilter(name: string): boolean {\n return !SENSITIVE_HEADERS.has(name.toLowerCase());\n}\n\n/**\n * Default body field filter function.\n */\nfunction defaultBodyFilter(key: string): boolean {\n return !SENSITIVE_BODY_FIELDS.has(key.toLowerCase());\n}\n\n/**\n * Filter an object's keys based on a filter function.\n */\nfunction filterObject<T extends Record<string, unknown>>(\n obj: T,\n filter: (key: string, value: unknown) => boolean\n): Partial<T> {\n const result: Partial<T> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (filter(key, value)) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Extract request context from a Fastify request.\n */\nfunction extractRequestContext(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): RequestContext {\n const headerFilter = options.filterHeaders || defaultHeaderFilter;\n const bodyFilter = options.filterBody || defaultBodyFilter;\n\n // Filter headers\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n if (typeof value === \"string\" && headerFilter(name, value)) {\n headers[name] = value;\n } else if (Array.isArray(value)) {\n const filtered = value.filter((v) => headerFilter(name, v));\n if (filtered.length > 0) {\n headers[name] = filtered.join(\", \");\n }\n }\n }\n\n const context: RequestContext = {\n url: request.url,\n method: request.method,\n headers,\n query_string: request.url.includes(\"?\") ? request.url.split(\"?\")[1] : undefined,\n };\n\n // Include body if requested and available\n if (options.includeBody && request.body && typeof request.body === \"object\") {\n context.data = filterObject(request.body as Record<string, unknown>, bodyFilter);\n }\n\n return context;\n}\n\n/**\n * Extract client IP from request.\n */\nfunction extractClientIp(request: FastifyRequest): string | undefined {\n // Check common proxy headers\n const forwarded = request.headers[\"x-forwarded-for\"];\n if (typeof forwarded === \"string\") {\n const firstIp = forwarded.split(\",\")[0]?.trim();\n if (firstIp) return firstIp;\n }\n\n const realIp = request.headers[\"x-real-ip\"];\n if (typeof realIp === \"string\") return realIp;\n\n const cfIp = request.headers[\"cf-connecting-ip\"];\n if (typeof cfIp === \"string\") return cfIp;\n\n // Fall back to connection address\n return request.ip;\n}\n\n/**\n * Create Bugwatch decorator for a request.\n */\nfunction createBugwatchDecorator(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): BugwatchDecorator {\n let eventId: string | undefined;\n const client = getClient();\n\n return {\n captureError(error: Error): string {\n if (!client) return \"\";\n\n const requestContext = extractRequestContext(request, options);\n const clientIp = extractClientIp(request);\n\n eventId = captureException(error, {\n request: requestContext,\n extra: {\n request: requestContext,\n ...(clientIp && { client_ip: clientIp }),\n },\n tags: {\n \"http.method\": request.method,\n \"http.url\": request.url,\n },\n });\n\n return eventId;\n },\n\n captureMessage(message: string, level: \"debug\" | \"info\" | \"warning\" | \"error\" = \"info\"): string {\n if (!client) return \"\";\n\n eventId = coreCaptureMessage(message, level);\n return eventId;\n },\n\n getEventId(): string | undefined {\n return eventId;\n },\n\n get client(): BugwatchClient | null {\n return client;\n },\n };\n}\n\n/**\n * Bugwatch plugin implementation.\n */\nconst bugwatchPluginImpl: FastifyPluginAsync<BugwatchFastifyOptions> = async (\n fastify,\n options\n) => {\n const opts: BugwatchFastifyOptions = {\n addBreadcrumbs: true,\n captureErrors: true,\n includeBody: false,\n flushOnError: false,\n ...options,\n };\n\n // Store request start times and scoped contexts\n const requestStartTimes = new WeakMap<FastifyRequest, number>();\n const requestContexts = new WeakMap<FastifyRequest, ScopedContext>();\n\n // Decorate requests with bugwatch\n fastify.decorateRequest(\"bugwatch\", null);\n\n // onRequest hook - set up context\n fastify.addHook(\"onRequest\", async (request) => {\n const client = getClient();\n if (!client) return;\n\n // Store start time\n requestStartTimes.set(request, Date.now());\n\n // Create a request-scoped context for isolation\n const scopedContext: ScopedContext = createScopedContext();\n requestContexts.set(request, scopedContext);\n\n // Create and attach decorator\n request.bugwatch = createBugwatchDecorator(request, opts);\n\n // Extract and set user context in the scoped context (not globally!)\n if (opts.extractUser) {\n const user = opts.extractUser(request);\n if (user) {\n scopedContext.user = user;\n }\n }\n\n // Add request breadcrumb to scoped context\n if (opts.addBreadcrumbs) {\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url}`,\n level: \"info\",\n data: {\n method: request.method,\n url: request.url,\n },\n });\n }\n });\n\n // onResponse hook - add completion breadcrumb\n fastify.addHook(\"onResponse\", async (request, reply) => {\n if (!opts.addBreadcrumbs) return;\n\n const client = getClient();\n if (!client) return;\n\n const startTime = requestStartTimes.get(request);\n const duration = startTime ? Date.now() - startTime : undefined;\n\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url} -> ${reply.statusCode}`,\n level: reply.statusCode >= 500 ? \"error\" : reply.statusCode >= 400 ? \"warning\" : \"info\",\n data: {\n method: request.method,\n url: request.url,\n status_code: reply.statusCode,\n ...(duration !== undefined && { duration_ms: duration }),\n },\n });\n });\n\n // onError hook - capture errors\n fastify.addHook(\"onError\", async (request, reply, error) => {\n if (!opts.captureErrors) return;\n\n const client = getClient();\n if (!client) return;\n\n // Capture the error\n request.bugwatch?.captureError(error);\n\n // Flush if requested\n if (opts.flushOnError) {\n await flush();\n }\n });\n};\n\n/**\n * Bugwatch Fastify plugin.\n *\n * This plugin automatically:\n * - Captures request context for errors\n * - Adds HTTP request breadcrumbs\n * - Reports unhandled errors to Bugwatch\n * - Provides `request.bugwatch` decorator for manual error capture\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * // Initialize Bugwatch\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n *\n * // Register the plugin\n * await fastify.register(bugwatchPlugin, {\n * extractUser: (request) => {\n * // Extract user from your auth system\n * return request.user ? { id: request.user.id } : null;\n * },\n * });\n *\n * // Your routes\n * fastify.get(\"/\", async (request, reply) => {\n * return { hello: \"world\" };\n * });\n *\n * // Manual error capture\n * fastify.get(\"/risky\", async (request, reply) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport const bugwatchPlugin = fp(bugwatchPluginImpl, {\n fastify: \"4.x || 5.x\",\n name: \"@bugwatch/fastify\",\n});\n"],"mappings":"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","names":["import_core","coreCaptureMessage","fp","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/setup.ts","../src/plugin.ts"],"sourcesContent":["/**\n * @bugwatch/fastify - Fastify integration for Bugwatch\n *\n * This package provides a plugin for automatically capturing errors\n * and request context in Fastify applications.\n *\n * @example One-liner setup (recommended)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Single call handles everything (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n *\n * @example Manual setup (for more control)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n * await fastify.register(bugwatchPlugin);\n *\n * // request.bugwatch is available for manual capture\n * fastify.get(\"/risky\", async (request) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\n\n// Export one-liner setup\nexport { setup } from \"./setup\";\nexport type { BugwatchFastifySetupOptions } from \"./setup\";\n\n// Export plugin\nexport { bugwatchPlugin } from \"./plugin\";\n\n// Export types\nexport type {\n BugwatchFastifyOptions,\n BugwatchDecorator,\n} from \"./types\";\n\n// Re-export core functions for convenience\nexport {\n init,\n getClient,\n captureException,\n captureMessage,\n addBreadcrumb,\n setUser,\n setTag,\n setExtra,\n flush,\n close,\n} from \"@bugwatch/core\";\n","/**\n * One-liner setup for Bugwatch Fastify integration.\n *\n * This module provides a simplified setup function that handles all\n * initialization and plugin registration in a single call.\n */\n\nimport type { FastifyInstance } from \"fastify\";\nimport { init, getClient, type BugwatchOptions } from \"@bugwatch/core\";\nimport { bugwatchPlugin } from \"./plugin\";\nimport type { BugwatchFastifyOptions } from \"./types\";\n\n/**\n * Combined options for Fastify setup.\n * Includes both core SDK options and Fastify-specific plugin options.\n */\nexport interface BugwatchFastifySetupOptions\n extends BugwatchFastifyOptions,\n Partial<BugwatchOptions> {}\n\n/**\n * Set up Bugwatch for a Fastify application with a single call.\n *\n * This function:\n * 1. Initializes the Bugwatch SDK (using env vars if no apiKey provided)\n * 2. Registers the Bugwatch plugin with the Fastify instance\n *\n * @param fastify - The Fastify instance\n * @param options - Configuration options (optional if BUGWATCH_API_KEY env var is set)\n * @returns Promise that resolves when the plugin is registered\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Minimal setup (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * environment: \"production\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport async function setup(\n fastify: FastifyInstance,\n options?: BugwatchFastifySetupOptions\n): Promise<void> {\n // Initialize core SDK if not already initialized\n if (!getClient()) {\n // Extract core options\n const coreOptions: Partial<BugwatchOptions> = {};\n if (options?.apiKey) coreOptions.apiKey = options.apiKey;\n if (options?.endpoint) coreOptions.endpoint = options.endpoint;\n if (options?.environment) coreOptions.environment = options.environment;\n if (options?.release) coreOptions.release = options.release;\n if (options?.debug !== undefined) coreOptions.debug = options.debug;\n if (options?.sampleRate !== undefined) coreOptions.sampleRate = options.sampleRate;\n if (options?.maxBreadcrumbs !== undefined) coreOptions.maxBreadcrumbs = options.maxBreadcrumbs;\n if (options?.tags) coreOptions.tags = options.tags;\n if (options?.user) coreOptions.user = options.user;\n if (options?.beforeSend) coreOptions.beforeSend = options.beforeSend;\n if (options?.ignoreErrors) coreOptions.ignoreErrors = options.ignoreErrors;\n\n init(coreOptions);\n }\n\n // Extract plugin-specific options\n const pluginOptions: BugwatchFastifyOptions = {};\n if (options?.extractUser) pluginOptions.extractUser = options.extractUser;\n if (options?.filterHeaders) pluginOptions.filterHeaders = options.filterHeaders;\n if (options?.filterBody) pluginOptions.filterBody = options.filterBody;\n if (options?.includeBody !== undefined) pluginOptions.includeBody = options.includeBody;\n if (options?.addBreadcrumbs !== undefined) pluginOptions.addBreadcrumbs = options.addBreadcrumbs;\n if (options?.captureErrors !== undefined) pluginOptions.captureErrors = options.captureErrors;\n if (options?.flushOnError !== undefined) pluginOptions.flushOnError = options.flushOnError;\n\n // Register the plugin\n await fastify.register(bugwatchPlugin, pluginOptions);\n}\n","import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport {\n getClient,\n captureException,\n captureMessage as coreCaptureMessage,\n addBreadcrumb,\n setUser,\n flush,\n close,\n runWithContext,\n createScopedContext,\n type RequestContext,\n type BugwatchClient,\n type ScopedContext,\n} from \"@bugwatch/core\";\nimport type { BugwatchFastifyOptions, BugwatchDecorator } from \"./types\";\n\n/**\n * Headers that should be filtered out by default for security.\n */\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n]);\n\n/**\n * Body fields that should be filtered out by default for security.\n */\nconst SENSITIVE_BODY_FIELDS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"credit_card\",\n \"creditCard\",\n \"ssn\",\n \"social_security\",\n]);\n\n/**\n * Default header filter function.\n */\nfunction defaultHeaderFilter(name: string): boolean {\n return !SENSITIVE_HEADERS.has(name.toLowerCase());\n}\n\n/**\n * Default body field filter function.\n */\nfunction defaultBodyFilter(key: string): boolean {\n return !SENSITIVE_BODY_FIELDS.has(key.toLowerCase());\n}\n\n/**\n * Filter an object's keys based on a filter function.\n */\nfunction filterObject<T extends Record<string, unknown>>(\n obj: T,\n filter: (key: string, value: unknown) => boolean\n): Partial<T> {\n const result: Partial<T> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (filter(key, value)) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Extract request context from a Fastify request.\n */\nfunction extractRequestContext(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): RequestContext {\n const headerFilter = options.filterHeaders || defaultHeaderFilter;\n const bodyFilter = options.filterBody || defaultBodyFilter;\n\n // Filter headers\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n if (typeof value === \"string\" && headerFilter(name, value)) {\n headers[name] = value;\n } else if (Array.isArray(value)) {\n const filtered = value.filter((v) => headerFilter(name, v));\n if (filtered.length > 0) {\n headers[name] = filtered.join(\", \");\n }\n }\n }\n\n const context: RequestContext = {\n url: request.url,\n method: request.method,\n headers,\n query_string: request.url.includes(\"?\") ? request.url.split(\"?\")[1] : undefined,\n };\n\n // Include body if requested and available\n if (options.includeBody && request.body && typeof request.body === \"object\") {\n context.data = filterObject(request.body as Record<string, unknown>, bodyFilter);\n }\n\n return context;\n}\n\n/**\n * Extract client IP from request.\n */\nfunction extractClientIp(request: FastifyRequest): string | undefined {\n // Check common proxy headers\n const forwarded = request.headers[\"x-forwarded-for\"];\n if (typeof forwarded === \"string\") {\n const firstIp = forwarded.split(\",\")[0]?.trim();\n if (firstIp) return firstIp;\n }\n\n const realIp = request.headers[\"x-real-ip\"];\n if (typeof realIp === \"string\") return realIp;\n\n const cfIp = request.headers[\"cf-connecting-ip\"];\n if (typeof cfIp === \"string\") return cfIp;\n\n // Fall back to connection address\n return request.ip;\n}\n\n/**\n * Create Bugwatch decorator for a request.\n */\nfunction createBugwatchDecorator(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): BugwatchDecorator {\n let eventId: string | undefined;\n const client = getClient();\n\n return {\n captureError(error: Error): string {\n if (!client) return \"\";\n\n const requestContext = extractRequestContext(request, options);\n const clientIp = extractClientIp(request);\n\n eventId = captureException(error, {\n request: requestContext,\n extra: {\n request: requestContext,\n ...(clientIp && { client_ip: clientIp }),\n },\n tags: {\n \"http.method\": request.method,\n \"http.url\": request.url,\n },\n });\n\n return eventId;\n },\n\n captureMessage(message: string, level: \"debug\" | \"info\" | \"warning\" | \"error\" = \"info\"): string {\n if (!client) return \"\";\n\n eventId = coreCaptureMessage(message, level);\n return eventId;\n },\n\n getEventId(): string | undefined {\n return eventId;\n },\n\n get client(): BugwatchClient | null {\n return client;\n },\n };\n}\n\n/**\n * Bugwatch plugin implementation.\n */\nconst bugwatchPluginImpl: FastifyPluginAsync<BugwatchFastifyOptions> = async (\n fastify,\n options\n) => {\n const opts: BugwatchFastifyOptions = {\n addBreadcrumbs: true,\n captureErrors: true,\n includeBody: false,\n flushOnError: false,\n ...options,\n };\n\n // Store request start times\n const requestStartTimes = new WeakMap<FastifyRequest, number>();\n\n // Decorate requests with bugwatch\n fastify.decorateRequest(\"bugwatch\", null);\n\n // onRequest hook — create scoped context and run the entire request lifecycle inside it.\n // We use runWithContext so that all downstream hooks and the route handler\n // can access the request-scoped context via getRequestContext().\n fastify.addHook(\"onRequest\", (request, _reply, done) => {\n const client = getClient();\n if (!client) {\n return done();\n }\n\n // Store start time\n requestStartTimes.set(request, Date.now());\n\n // Create a request-scoped context for isolation\n const scopedContext: ScopedContext = createScopedContext();\n\n // Create and attach decorator\n request.bugwatch = createBugwatchDecorator(request, opts);\n\n // Extract and set user context in the scoped context (not globally!)\n if (opts.extractUser) {\n const user = opts.extractUser(request);\n if (user) {\n scopedContext.user = user;\n }\n }\n\n // Run done() inside the scoped context so that all downstream\n // hooks and route handlers inherit the AsyncLocalStorage context\n runWithContext(scopedContext, () => {\n // Add request breadcrumb within the scoped context\n if (opts.addBreadcrumbs) {\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url}`,\n level: \"info\",\n data: {\n method: request.method,\n url: request.url,\n },\n });\n }\n\n done();\n });\n });\n\n // onResponse hook - add completion breadcrumb\n fastify.addHook(\"onResponse\", async (request, reply) => {\n if (!opts.addBreadcrumbs) return;\n\n const client = getClient();\n if (!client) return;\n\n const startTime = requestStartTimes.get(request);\n const duration = startTime ? Date.now() - startTime : undefined;\n\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url} -> ${reply.statusCode}`,\n level: reply.statusCode >= 500 ? \"error\" : reply.statusCode >= 400 ? \"warning\" : \"info\",\n data: {\n method: request.method,\n url: request.url,\n status_code: reply.statusCode,\n ...(duration !== undefined && { duration_ms: duration }),\n },\n });\n });\n\n // onError hook - capture errors\n fastify.addHook(\"onError\", async (request, _reply, error) => {\n if (!opts.captureErrors) return;\n\n const client = getClient();\n if (!client) return;\n\n // Capture the error\n request.bugwatch?.captureError(error);\n\n // Flush if requested\n if (opts.flushOnError) {\n await flush();\n }\n });\n\n // onClose hook - flush pending events on server shutdown\n fastify.addHook(\"onClose\", async () => {\n await close();\n });\n};\n\n/**\n * Bugwatch Fastify plugin.\n *\n * This plugin automatically:\n * - Captures request context for errors\n * - Adds HTTP request breadcrumbs\n * - Reports unhandled errors to Bugwatch\n * - Provides `request.bugwatch` decorator for manual error capture\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * // Initialize Bugwatch\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n *\n * // Register the plugin\n * await fastify.register(bugwatchPlugin, {\n * extractUser: (request) => {\n * // Extract user from your auth system\n * return request.user ? { id: request.user.id } : null;\n * },\n * });\n *\n * // Your routes\n * fastify.get(\"/\", async (request, reply) => {\n * return { hello: \"world\" };\n * });\n *\n * // Manual error capture\n * fastify.get(\"/risky\", async (request, reply) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport const bugwatchPlugin = fp(bugwatchPluginImpl, {\n fastify: \"4.x || 5.x\",\n name: \"@bugwatch/fastify\",\n});\n"],"mappings":"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","names":["import_core","coreCaptureMessage","fp","import_core"]}
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
captureMessage as coreCaptureMessage,
|
|
10
10
|
addBreadcrumb,
|
|
11
11
|
flush,
|
|
12
|
+
close,
|
|
13
|
+
runWithContext,
|
|
12
14
|
createScopedContext
|
|
13
15
|
} from "@bugwatch/core";
|
|
14
16
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -127,14 +129,14 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
127
129
|
...options
|
|
128
130
|
};
|
|
129
131
|
const requestStartTimes = /* @__PURE__ */ new WeakMap();
|
|
130
|
-
const requestContexts = /* @__PURE__ */ new WeakMap();
|
|
131
132
|
fastify.decorateRequest("bugwatch", null);
|
|
132
|
-
fastify.addHook("onRequest",
|
|
133
|
+
fastify.addHook("onRequest", (request, _reply, done) => {
|
|
133
134
|
const client = getClient();
|
|
134
|
-
if (!client)
|
|
135
|
+
if (!client) {
|
|
136
|
+
return done();
|
|
137
|
+
}
|
|
135
138
|
requestStartTimes.set(request, Date.now());
|
|
136
139
|
const scopedContext = createScopedContext();
|
|
137
|
-
requestContexts.set(request, scopedContext);
|
|
138
140
|
request.bugwatch = createBugwatchDecorator(request, opts);
|
|
139
141
|
if (opts.extractUser) {
|
|
140
142
|
const user = opts.extractUser(request);
|
|
@@ -142,17 +144,20 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
142
144
|
scopedContext.user = user;
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
runWithContext(scopedContext, () => {
|
|
148
|
+
if (opts.addBreadcrumbs) {
|
|
149
|
+
addBreadcrumb({
|
|
150
|
+
category: "http",
|
|
151
|
+
message: `${request.method} ${request.url}`,
|
|
152
|
+
level: "info",
|
|
153
|
+
data: {
|
|
154
|
+
method: request.method,
|
|
155
|
+
url: request.url
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
done();
|
|
160
|
+
});
|
|
156
161
|
});
|
|
157
162
|
fastify.addHook("onResponse", async (request, reply) => {
|
|
158
163
|
if (!opts.addBreadcrumbs) return;
|
|
@@ -172,7 +177,7 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
172
177
|
}
|
|
173
178
|
});
|
|
174
179
|
});
|
|
175
|
-
fastify.addHook("onError", async (request,
|
|
180
|
+
fastify.addHook("onError", async (request, _reply, error) => {
|
|
176
181
|
if (!opts.captureErrors) return;
|
|
177
182
|
const client = getClient();
|
|
178
183
|
if (!client) return;
|
|
@@ -181,6 +186,9 @@ var bugwatchPluginImpl = async (fastify, options) => {
|
|
|
181
186
|
await flush();
|
|
182
187
|
}
|
|
183
188
|
});
|
|
189
|
+
fastify.addHook("onClose", async () => {
|
|
190
|
+
await close();
|
|
191
|
+
});
|
|
184
192
|
};
|
|
185
193
|
var bugwatchPlugin = fp(bugwatchPluginImpl, {
|
|
186
194
|
fastify: "4.x || 5.x",
|
|
@@ -226,14 +234,14 @@ import {
|
|
|
226
234
|
setTag,
|
|
227
235
|
setExtra,
|
|
228
236
|
flush as flush2,
|
|
229
|
-
close
|
|
237
|
+
close as close2
|
|
230
238
|
} from "@bugwatch/core";
|
|
231
239
|
export {
|
|
232
240
|
addBreadcrumb2 as addBreadcrumb,
|
|
233
241
|
bugwatchPlugin,
|
|
234
242
|
captureException2 as captureException,
|
|
235
243
|
captureMessage,
|
|
236
|
-
close,
|
|
244
|
+
close2 as close,
|
|
237
245
|
flush2 as flush,
|
|
238
246
|
getClient3 as getClient,
|
|
239
247
|
init2 as init,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/setup.ts","../src/plugin.ts","../src/index.ts"],"sourcesContent":["/**\n * One-liner setup for Bugwatch Fastify integration.\n *\n * This module provides a simplified setup function that handles all\n * initialization and plugin registration in a single call.\n */\n\nimport type { FastifyInstance } from \"fastify\";\nimport { init, getClient, type BugwatchOptions } from \"@bugwatch/core\";\nimport { bugwatchPlugin } from \"./plugin\";\nimport type { BugwatchFastifyOptions } from \"./types\";\n\n/**\n * Combined options for Fastify setup.\n * Includes both core SDK options and Fastify-specific plugin options.\n */\nexport interface BugwatchFastifySetupOptions\n extends BugwatchFastifyOptions,\n Partial<BugwatchOptions> {}\n\n/**\n * Set up Bugwatch for a Fastify application with a single call.\n *\n * This function:\n * 1. Initializes the Bugwatch SDK (using env vars if no apiKey provided)\n * 2. Registers the Bugwatch plugin with the Fastify instance\n *\n * @param fastify - The Fastify instance\n * @param options - Configuration options (optional if BUGWATCH_API_KEY env var is set)\n * @returns Promise that resolves when the plugin is registered\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Minimal setup (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * environment: \"production\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport async function setup(\n fastify: FastifyInstance,\n options?: BugwatchFastifySetupOptions\n): Promise<void> {\n // Initialize core SDK if not already initialized\n if (!getClient()) {\n // Extract core options\n const coreOptions: Partial<BugwatchOptions> = {};\n if (options?.apiKey) coreOptions.apiKey = options.apiKey;\n if (options?.endpoint) coreOptions.endpoint = options.endpoint;\n if (options?.environment) coreOptions.environment = options.environment;\n if (options?.release) coreOptions.release = options.release;\n if (options?.debug !== undefined) coreOptions.debug = options.debug;\n if (options?.sampleRate !== undefined) coreOptions.sampleRate = options.sampleRate;\n if (options?.maxBreadcrumbs !== undefined) coreOptions.maxBreadcrumbs = options.maxBreadcrumbs;\n if (options?.tags) coreOptions.tags = options.tags;\n if (options?.user) coreOptions.user = options.user;\n if (options?.beforeSend) coreOptions.beforeSend = options.beforeSend;\n if (options?.ignoreErrors) coreOptions.ignoreErrors = options.ignoreErrors;\n\n init(coreOptions);\n }\n\n // Extract plugin-specific options\n const pluginOptions: BugwatchFastifyOptions = {};\n if (options?.extractUser) pluginOptions.extractUser = options.extractUser;\n if (options?.filterHeaders) pluginOptions.filterHeaders = options.filterHeaders;\n if (options?.filterBody) pluginOptions.filterBody = options.filterBody;\n if (options?.includeBody !== undefined) pluginOptions.includeBody = options.includeBody;\n if (options?.addBreadcrumbs !== undefined) pluginOptions.addBreadcrumbs = options.addBreadcrumbs;\n if (options?.captureErrors !== undefined) pluginOptions.captureErrors = options.captureErrors;\n if (options?.flushOnError !== undefined) pluginOptions.flushOnError = options.flushOnError;\n\n // Register the plugin\n await fastify.register(bugwatchPlugin, pluginOptions);\n}\n","import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport {\n getClient,\n captureException,\n captureMessage as coreCaptureMessage,\n addBreadcrumb,\n setUser,\n flush,\n runWithContextAsync,\n createScopedContext,\n type RequestContext,\n type BugwatchClient,\n type ScopedContext,\n} from \"@bugwatch/core\";\nimport type { BugwatchFastifyOptions, BugwatchDecorator } from \"./types\";\n\n/**\n * Headers that should be filtered out by default for security.\n */\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n]);\n\n/**\n * Body fields that should be filtered out by default for security.\n */\nconst SENSITIVE_BODY_FIELDS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"credit_card\",\n \"creditCard\",\n \"ssn\",\n \"social_security\",\n]);\n\n/**\n * Default header filter function.\n */\nfunction defaultHeaderFilter(name: string): boolean {\n return !SENSITIVE_HEADERS.has(name.toLowerCase());\n}\n\n/**\n * Default body field filter function.\n */\nfunction defaultBodyFilter(key: string): boolean {\n return !SENSITIVE_BODY_FIELDS.has(key.toLowerCase());\n}\n\n/**\n * Filter an object's keys based on a filter function.\n */\nfunction filterObject<T extends Record<string, unknown>>(\n obj: T,\n filter: (key: string, value: unknown) => boolean\n): Partial<T> {\n const result: Partial<T> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (filter(key, value)) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Extract request context from a Fastify request.\n */\nfunction extractRequestContext(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): RequestContext {\n const headerFilter = options.filterHeaders || defaultHeaderFilter;\n const bodyFilter = options.filterBody || defaultBodyFilter;\n\n // Filter headers\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n if (typeof value === \"string\" && headerFilter(name, value)) {\n headers[name] = value;\n } else if (Array.isArray(value)) {\n const filtered = value.filter((v) => headerFilter(name, v));\n if (filtered.length > 0) {\n headers[name] = filtered.join(\", \");\n }\n }\n }\n\n const context: RequestContext = {\n url: request.url,\n method: request.method,\n headers,\n query_string: request.url.includes(\"?\") ? request.url.split(\"?\")[1] : undefined,\n };\n\n // Include body if requested and available\n if (options.includeBody && request.body && typeof request.body === \"object\") {\n context.data = filterObject(request.body as Record<string, unknown>, bodyFilter);\n }\n\n return context;\n}\n\n/**\n * Extract client IP from request.\n */\nfunction extractClientIp(request: FastifyRequest): string | undefined {\n // Check common proxy headers\n const forwarded = request.headers[\"x-forwarded-for\"];\n if (typeof forwarded === \"string\") {\n const firstIp = forwarded.split(\",\")[0]?.trim();\n if (firstIp) return firstIp;\n }\n\n const realIp = request.headers[\"x-real-ip\"];\n if (typeof realIp === \"string\") return realIp;\n\n const cfIp = request.headers[\"cf-connecting-ip\"];\n if (typeof cfIp === \"string\") return cfIp;\n\n // Fall back to connection address\n return request.ip;\n}\n\n/**\n * Create Bugwatch decorator for a request.\n */\nfunction createBugwatchDecorator(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): BugwatchDecorator {\n let eventId: string | undefined;\n const client = getClient();\n\n return {\n captureError(error: Error): string {\n if (!client) return \"\";\n\n const requestContext = extractRequestContext(request, options);\n const clientIp = extractClientIp(request);\n\n eventId = captureException(error, {\n request: requestContext,\n extra: {\n request: requestContext,\n ...(clientIp && { client_ip: clientIp }),\n },\n tags: {\n \"http.method\": request.method,\n \"http.url\": request.url,\n },\n });\n\n return eventId;\n },\n\n captureMessage(message: string, level: \"debug\" | \"info\" | \"warning\" | \"error\" = \"info\"): string {\n if (!client) return \"\";\n\n eventId = coreCaptureMessage(message, level);\n return eventId;\n },\n\n getEventId(): string | undefined {\n return eventId;\n },\n\n get client(): BugwatchClient | null {\n return client;\n },\n };\n}\n\n/**\n * Bugwatch plugin implementation.\n */\nconst bugwatchPluginImpl: FastifyPluginAsync<BugwatchFastifyOptions> = async (\n fastify,\n options\n) => {\n const opts: BugwatchFastifyOptions = {\n addBreadcrumbs: true,\n captureErrors: true,\n includeBody: false,\n flushOnError: false,\n ...options,\n };\n\n // Store request start times and scoped contexts\n const requestStartTimes = new WeakMap<FastifyRequest, number>();\n const requestContexts = new WeakMap<FastifyRequest, ScopedContext>();\n\n // Decorate requests with bugwatch\n fastify.decorateRequest(\"bugwatch\", null);\n\n // onRequest hook - set up context\n fastify.addHook(\"onRequest\", async (request) => {\n const client = getClient();\n if (!client) return;\n\n // Store start time\n requestStartTimes.set(request, Date.now());\n\n // Create a request-scoped context for isolation\n const scopedContext: ScopedContext = createScopedContext();\n requestContexts.set(request, scopedContext);\n\n // Create and attach decorator\n request.bugwatch = createBugwatchDecorator(request, opts);\n\n // Extract and set user context in the scoped context (not globally!)\n if (opts.extractUser) {\n const user = opts.extractUser(request);\n if (user) {\n scopedContext.user = user;\n }\n }\n\n // Add request breadcrumb to scoped context\n if (opts.addBreadcrumbs) {\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url}`,\n level: \"info\",\n data: {\n method: request.method,\n url: request.url,\n },\n });\n }\n });\n\n // onResponse hook - add completion breadcrumb\n fastify.addHook(\"onResponse\", async (request, reply) => {\n if (!opts.addBreadcrumbs) return;\n\n const client = getClient();\n if (!client) return;\n\n const startTime = requestStartTimes.get(request);\n const duration = startTime ? Date.now() - startTime : undefined;\n\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url} -> ${reply.statusCode}`,\n level: reply.statusCode >= 500 ? \"error\" : reply.statusCode >= 400 ? \"warning\" : \"info\",\n data: {\n method: request.method,\n url: request.url,\n status_code: reply.statusCode,\n ...(duration !== undefined && { duration_ms: duration }),\n },\n });\n });\n\n // onError hook - capture errors\n fastify.addHook(\"onError\", async (request, reply, error) => {\n if (!opts.captureErrors) return;\n\n const client = getClient();\n if (!client) return;\n\n // Capture the error\n request.bugwatch?.captureError(error);\n\n // Flush if requested\n if (opts.flushOnError) {\n await flush();\n }\n });\n};\n\n/**\n * Bugwatch Fastify plugin.\n *\n * This plugin automatically:\n * - Captures request context for errors\n * - Adds HTTP request breadcrumbs\n * - Reports unhandled errors to Bugwatch\n * - Provides `request.bugwatch` decorator for manual error capture\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * // Initialize Bugwatch\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n *\n * // Register the plugin\n * await fastify.register(bugwatchPlugin, {\n * extractUser: (request) => {\n * // Extract user from your auth system\n * return request.user ? { id: request.user.id } : null;\n * },\n * });\n *\n * // Your routes\n * fastify.get(\"/\", async (request, reply) => {\n * return { hello: \"world\" };\n * });\n *\n * // Manual error capture\n * fastify.get(\"/risky\", async (request, reply) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport const bugwatchPlugin = fp(bugwatchPluginImpl, {\n fastify: \"4.x || 5.x\",\n name: \"@bugwatch/fastify\",\n});\n","/**\n * @bugwatch/fastify - Fastify integration for Bugwatch\n *\n * This package provides a plugin for automatically capturing errors\n * and request context in Fastify applications.\n *\n * @example One-liner setup (recommended)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Single call handles everything (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n *\n * @example Manual setup (for more control)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n * await fastify.register(bugwatchPlugin);\n *\n * // request.bugwatch is available for manual capture\n * fastify.get(\"/risky\", async (request) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\n\n// Export one-liner setup\nexport { setup } from \"./setup\";\nexport type { BugwatchFastifySetupOptions } from \"./setup\";\n\n// Export plugin\nexport { bugwatchPlugin } from \"./plugin\";\n\n// Export types\nexport type {\n BugwatchFastifyOptions,\n BugwatchDecorator,\n} from \"./types\";\n\n// Re-export core functions for convenience\nexport {\n init,\n getClient,\n captureException,\n captureMessage,\n addBreadcrumb,\n setUser,\n setTag,\n setExtra,\n flush,\n close,\n} from \"@bugwatch/core\";\n"],"mappings":"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","names":["getClient","getClient","init","getClient","captureException","addBreadcrumb","setUser","flush"]}
|
|
1
|
+
{"version":3,"sources":["../src/setup.ts","../src/plugin.ts","../src/index.ts"],"sourcesContent":["/**\n * One-liner setup for Bugwatch Fastify integration.\n *\n * This module provides a simplified setup function that handles all\n * initialization and plugin registration in a single call.\n */\n\nimport type { FastifyInstance } from \"fastify\";\nimport { init, getClient, type BugwatchOptions } from \"@bugwatch/core\";\nimport { bugwatchPlugin } from \"./plugin\";\nimport type { BugwatchFastifyOptions } from \"./types\";\n\n/**\n * Combined options for Fastify setup.\n * Includes both core SDK options and Fastify-specific plugin options.\n */\nexport interface BugwatchFastifySetupOptions\n extends BugwatchFastifyOptions,\n Partial<BugwatchOptions> {}\n\n/**\n * Set up Bugwatch for a Fastify application with a single call.\n *\n * This function:\n * 1. Initializes the Bugwatch SDK (using env vars if no apiKey provided)\n * 2. Registers the Bugwatch plugin with the Fastify instance\n *\n * @param fastify - The Fastify instance\n * @param options - Configuration options (optional if BUGWATCH_API_KEY env var is set)\n * @returns Promise that resolves when the plugin is registered\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Minimal setup (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * environment: \"production\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport async function setup(\n fastify: FastifyInstance,\n options?: BugwatchFastifySetupOptions\n): Promise<void> {\n // Initialize core SDK if not already initialized\n if (!getClient()) {\n // Extract core options\n const coreOptions: Partial<BugwatchOptions> = {};\n if (options?.apiKey) coreOptions.apiKey = options.apiKey;\n if (options?.endpoint) coreOptions.endpoint = options.endpoint;\n if (options?.environment) coreOptions.environment = options.environment;\n if (options?.release) coreOptions.release = options.release;\n if (options?.debug !== undefined) coreOptions.debug = options.debug;\n if (options?.sampleRate !== undefined) coreOptions.sampleRate = options.sampleRate;\n if (options?.maxBreadcrumbs !== undefined) coreOptions.maxBreadcrumbs = options.maxBreadcrumbs;\n if (options?.tags) coreOptions.tags = options.tags;\n if (options?.user) coreOptions.user = options.user;\n if (options?.beforeSend) coreOptions.beforeSend = options.beforeSend;\n if (options?.ignoreErrors) coreOptions.ignoreErrors = options.ignoreErrors;\n\n init(coreOptions);\n }\n\n // Extract plugin-specific options\n const pluginOptions: BugwatchFastifyOptions = {};\n if (options?.extractUser) pluginOptions.extractUser = options.extractUser;\n if (options?.filterHeaders) pluginOptions.filterHeaders = options.filterHeaders;\n if (options?.filterBody) pluginOptions.filterBody = options.filterBody;\n if (options?.includeBody !== undefined) pluginOptions.includeBody = options.includeBody;\n if (options?.addBreadcrumbs !== undefined) pluginOptions.addBreadcrumbs = options.addBreadcrumbs;\n if (options?.captureErrors !== undefined) pluginOptions.captureErrors = options.captureErrors;\n if (options?.flushOnError !== undefined) pluginOptions.flushOnError = options.flushOnError;\n\n // Register the plugin\n await fastify.register(bugwatchPlugin, pluginOptions);\n}\n","import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport {\n getClient,\n captureException,\n captureMessage as coreCaptureMessage,\n addBreadcrumb,\n setUser,\n flush,\n close,\n runWithContext,\n createScopedContext,\n type RequestContext,\n type BugwatchClient,\n type ScopedContext,\n} from \"@bugwatch/core\";\nimport type { BugwatchFastifyOptions, BugwatchDecorator } from \"./types\";\n\n/**\n * Headers that should be filtered out by default for security.\n */\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n \"proxy-authorization\",\n]);\n\n/**\n * Body fields that should be filtered out by default for security.\n */\nconst SENSITIVE_BODY_FIELDS = new Set([\n \"password\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apiKey\",\n \"credit_card\",\n \"creditCard\",\n \"ssn\",\n \"social_security\",\n]);\n\n/**\n * Default header filter function.\n */\nfunction defaultHeaderFilter(name: string): boolean {\n return !SENSITIVE_HEADERS.has(name.toLowerCase());\n}\n\n/**\n * Default body field filter function.\n */\nfunction defaultBodyFilter(key: string): boolean {\n return !SENSITIVE_BODY_FIELDS.has(key.toLowerCase());\n}\n\n/**\n * Filter an object's keys based on a filter function.\n */\nfunction filterObject<T extends Record<string, unknown>>(\n obj: T,\n filter: (key: string, value: unknown) => boolean\n): Partial<T> {\n const result: Partial<T> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (filter(key, value)) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Extract request context from a Fastify request.\n */\nfunction extractRequestContext(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): RequestContext {\n const headerFilter = options.filterHeaders || defaultHeaderFilter;\n const bodyFilter = options.filterBody || defaultBodyFilter;\n\n // Filter headers\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n if (typeof value === \"string\" && headerFilter(name, value)) {\n headers[name] = value;\n } else if (Array.isArray(value)) {\n const filtered = value.filter((v) => headerFilter(name, v));\n if (filtered.length > 0) {\n headers[name] = filtered.join(\", \");\n }\n }\n }\n\n const context: RequestContext = {\n url: request.url,\n method: request.method,\n headers,\n query_string: request.url.includes(\"?\") ? request.url.split(\"?\")[1] : undefined,\n };\n\n // Include body if requested and available\n if (options.includeBody && request.body && typeof request.body === \"object\") {\n context.data = filterObject(request.body as Record<string, unknown>, bodyFilter);\n }\n\n return context;\n}\n\n/**\n * Extract client IP from request.\n */\nfunction extractClientIp(request: FastifyRequest): string | undefined {\n // Check common proxy headers\n const forwarded = request.headers[\"x-forwarded-for\"];\n if (typeof forwarded === \"string\") {\n const firstIp = forwarded.split(\",\")[0]?.trim();\n if (firstIp) return firstIp;\n }\n\n const realIp = request.headers[\"x-real-ip\"];\n if (typeof realIp === \"string\") return realIp;\n\n const cfIp = request.headers[\"cf-connecting-ip\"];\n if (typeof cfIp === \"string\") return cfIp;\n\n // Fall back to connection address\n return request.ip;\n}\n\n/**\n * Create Bugwatch decorator for a request.\n */\nfunction createBugwatchDecorator(\n request: FastifyRequest,\n options: BugwatchFastifyOptions\n): BugwatchDecorator {\n let eventId: string | undefined;\n const client = getClient();\n\n return {\n captureError(error: Error): string {\n if (!client) return \"\";\n\n const requestContext = extractRequestContext(request, options);\n const clientIp = extractClientIp(request);\n\n eventId = captureException(error, {\n request: requestContext,\n extra: {\n request: requestContext,\n ...(clientIp && { client_ip: clientIp }),\n },\n tags: {\n \"http.method\": request.method,\n \"http.url\": request.url,\n },\n });\n\n return eventId;\n },\n\n captureMessage(message: string, level: \"debug\" | \"info\" | \"warning\" | \"error\" = \"info\"): string {\n if (!client) return \"\";\n\n eventId = coreCaptureMessage(message, level);\n return eventId;\n },\n\n getEventId(): string | undefined {\n return eventId;\n },\n\n get client(): BugwatchClient | null {\n return client;\n },\n };\n}\n\n/**\n * Bugwatch plugin implementation.\n */\nconst bugwatchPluginImpl: FastifyPluginAsync<BugwatchFastifyOptions> = async (\n fastify,\n options\n) => {\n const opts: BugwatchFastifyOptions = {\n addBreadcrumbs: true,\n captureErrors: true,\n includeBody: false,\n flushOnError: false,\n ...options,\n };\n\n // Store request start times\n const requestStartTimes = new WeakMap<FastifyRequest, number>();\n\n // Decorate requests with bugwatch\n fastify.decorateRequest(\"bugwatch\", null);\n\n // onRequest hook — create scoped context and run the entire request lifecycle inside it.\n // We use runWithContext so that all downstream hooks and the route handler\n // can access the request-scoped context via getRequestContext().\n fastify.addHook(\"onRequest\", (request, _reply, done) => {\n const client = getClient();\n if (!client) {\n return done();\n }\n\n // Store start time\n requestStartTimes.set(request, Date.now());\n\n // Create a request-scoped context for isolation\n const scopedContext: ScopedContext = createScopedContext();\n\n // Create and attach decorator\n request.bugwatch = createBugwatchDecorator(request, opts);\n\n // Extract and set user context in the scoped context (not globally!)\n if (opts.extractUser) {\n const user = opts.extractUser(request);\n if (user) {\n scopedContext.user = user;\n }\n }\n\n // Run done() inside the scoped context so that all downstream\n // hooks and route handlers inherit the AsyncLocalStorage context\n runWithContext(scopedContext, () => {\n // Add request breadcrumb within the scoped context\n if (opts.addBreadcrumbs) {\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url}`,\n level: \"info\",\n data: {\n method: request.method,\n url: request.url,\n },\n });\n }\n\n done();\n });\n });\n\n // onResponse hook - add completion breadcrumb\n fastify.addHook(\"onResponse\", async (request, reply) => {\n if (!opts.addBreadcrumbs) return;\n\n const client = getClient();\n if (!client) return;\n\n const startTime = requestStartTimes.get(request);\n const duration = startTime ? Date.now() - startTime : undefined;\n\n addBreadcrumb({\n category: \"http\",\n message: `${request.method} ${request.url} -> ${reply.statusCode}`,\n level: reply.statusCode >= 500 ? \"error\" : reply.statusCode >= 400 ? \"warning\" : \"info\",\n data: {\n method: request.method,\n url: request.url,\n status_code: reply.statusCode,\n ...(duration !== undefined && { duration_ms: duration }),\n },\n });\n });\n\n // onError hook - capture errors\n fastify.addHook(\"onError\", async (request, _reply, error) => {\n if (!opts.captureErrors) return;\n\n const client = getClient();\n if (!client) return;\n\n // Capture the error\n request.bugwatch?.captureError(error);\n\n // Flush if requested\n if (opts.flushOnError) {\n await flush();\n }\n });\n\n // onClose hook - flush pending events on server shutdown\n fastify.addHook(\"onClose\", async () => {\n await close();\n });\n};\n\n/**\n * Bugwatch Fastify plugin.\n *\n * This plugin automatically:\n * - Captures request context for errors\n * - Adds HTTP request breadcrumbs\n * - Reports unhandled errors to Bugwatch\n * - Provides `request.bugwatch` decorator for manual error capture\n *\n * @example\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * // Initialize Bugwatch\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n *\n * // Register the plugin\n * await fastify.register(bugwatchPlugin, {\n * extractUser: (request) => {\n * // Extract user from your auth system\n * return request.user ? { id: request.user.id } : null;\n * },\n * });\n *\n * // Your routes\n * fastify.get(\"/\", async (request, reply) => {\n * return { hello: \"world\" };\n * });\n *\n * // Manual error capture\n * fastify.get(\"/risky\", async (request, reply) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\nexport const bugwatchPlugin = fp(bugwatchPluginImpl, {\n fastify: \"4.x || 5.x\",\n name: \"@bugwatch/fastify\",\n});\n","/**\n * @bugwatch/fastify - Fastify integration for Bugwatch\n *\n * This package provides a plugin for automatically capturing errors\n * and request context in Fastify applications.\n *\n * @example One-liner setup (recommended)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { setup } from \"@bugwatch/fastify\";\n *\n * const fastify = Fastify();\n *\n * // Single call handles everything (uses BUGWATCH_API_KEY env var)\n * await setup(fastify);\n *\n * // Or with explicit options\n * await setup(fastify, {\n * apiKey: \"bw_live_xxxxx\",\n * extractUser: (request) => ({ id: request.user?.id }),\n * });\n *\n * fastify.get(\"/\", async () => ({ hello: \"world\" }));\n *\n * await fastify.listen({ port: 3000 });\n * ```\n *\n * @example Manual setup (for more control)\n * ```typescript\n * import Fastify from \"fastify\";\n * import { init } from \"@bugwatch/core\";\n * import { bugwatchPlugin } from \"@bugwatch/fastify\";\n *\n * init({ apiKey: \"your-api-key\" });\n *\n * const fastify = Fastify();\n * await fastify.register(bugwatchPlugin);\n *\n * // request.bugwatch is available for manual capture\n * fastify.get(\"/risky\", async (request) => {\n * try {\n * await riskyOperation();\n * } catch (err) {\n * request.bugwatch.captureError(err);\n * return { error: \"Something went wrong\" };\n * }\n * });\n *\n * await fastify.listen({ port: 3000 });\n * ```\n */\n\n// Export one-liner setup\nexport { setup } from \"./setup\";\nexport type { BugwatchFastifySetupOptions } from \"./setup\";\n\n// Export plugin\nexport { bugwatchPlugin } from \"./plugin\";\n\n// Export types\nexport type {\n BugwatchFastifyOptions,\n BugwatchDecorator,\n} from \"./types\";\n\n// Re-export core functions for convenience\nexport {\n init,\n getClient,\n captureException,\n captureMessage,\n addBreadcrumb,\n setUser,\n setTag,\n setExtra,\n flush,\n close,\n} from \"@bugwatch/core\";\n"],"mappings":"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","names":["getClient","getClient","init","getClient","captureException","addBreadcrumb","setUser","flush","close"]}
|