silgi 0.1.0-beta.2 → 0.1.0-beta.4

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.
@@ -232,7 +232,7 @@ location.reload();
232
232
  });
233
233
  const route = match.data;
234
234
  const method = request.method;
235
- if (ctxFactoryIsSync && (method === "GET" || !request.body || route.passthrough)) {
235
+ if (ctxFactoryIsSync && (method === "GET" || !request.body) && !route.passthrough) {
236
236
  const usePool = !ctxFactoryIsEmpty || match.params || collector;
237
237
  const ctx = usePool ? ctxPool.borrow() : emptyCtx;
238
238
  let t0 = 0;
@@ -415,7 +415,28 @@ location.reload();
415
415
  t0 = collector ? performance.now() : 0;
416
416
  const pipelineResult = route.handler(ctx, rawInput, request.signal);
417
417
  const output = pipelineResult instanceof Promise ? await pipelineResult : pipelineResult;
418
- if (output instanceof Response) return output;
418
+ if (output instanceof Response) {
419
+ if (hasHooks || collector) {
420
+ const durationMs = collector ? round(performance.now() - t0) : 0;
421
+ if (hasHooks) hooks.callHook("response", {
422
+ path: pathname,
423
+ output: null,
424
+ durationMs
425
+ });
426
+ if (collector) {
427
+ collector.record(pathname, durationMs);
428
+ if (accumulator) accumulator.addProcedure({
429
+ procedure: pathname,
430
+ durationMs,
431
+ status: output.status,
432
+ input: rawInput ?? reqTrace?.procedureInput ?? null,
433
+ output: reqTrace?.procedureOutput ?? null,
434
+ spans: reqTrace?.spans ?? []
435
+ });
436
+ }
437
+ }
438
+ return output;
439
+ }
419
440
  if (output instanceof ReadableStream) {
420
441
  if (collector) {
421
442
  const durationMs = round(performance.now() - t0);
@@ -0,0 +1,61 @@
1
+ //#region src/integrations/better-auth/index.d.ts
2
+ /**
3
+ * Silgi + Better Auth tracing integration.
4
+ *
5
+ * Provides a Better Auth plugin factory that auto-traces all auth operations
6
+ * (sign-in, sign-up, OAuth, session management, etc.) into silgi analytics.
7
+ *
8
+ * The silgi request context is passed via `request.__silgiCtx`, set by
9
+ * the silgi auth handler before calling `auth.handler(request)`.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { silgiTracing } from 'silgi/better-auth'
14
+ *
15
+ * const auth = betterAuth({
16
+ * plugins: [
17
+ * silgiTracing(), // auto-traces all auth operations
18
+ * ],
19
+ * })
20
+ * ```
21
+ */
22
+ interface SilgiTracingConfig {
23
+ /** Capture request body as span input (default: true) */
24
+ captureInput?: boolean;
25
+ /** Capture response data as span output (default: true) */
26
+ captureOutput?: boolean;
27
+ /** Pass `createAuthMiddleware` from `better-auth/api` to wrap hooks handler */
28
+ createAuthMiddleware?: (handler: any) => any;
29
+ }
30
+ /**
31
+ * Creates a Better Auth plugin that auto-traces all auth operations
32
+ * into silgi analytics spans.
33
+ *
34
+ * @param config - Optional configuration
35
+ * @returns A Better Auth plugin (typed as `any` to avoid requiring better-auth types at build time)
36
+ */
37
+ declare function silgiTracing(config?: SilgiTracingConfig): any;
38
+ /**
39
+ * Instrument a Better Auth instance to trace all `auth.api.*` method calls.
40
+ * Works with `withSilgiCtx` — programmatic calls from background jobs,
41
+ * server-side session fetches etc. are traced when context is available.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { instrumentBetterAuth, withSilgiCtx } from 'silgi/better-auth'
46
+ *
47
+ * const auth = instrumentBetterAuth(betterAuth({ ... }))
48
+ *
49
+ * // In a silgi procedure:
50
+ * const me = s.$resolve(async ({ ctx }) => {
51
+ * return withSilgiCtx(ctx, () => auth.api.getSession({ headers: ctx.headers }))
52
+ * })
53
+ * ```
54
+ */
55
+ declare function instrumentBetterAuth<T extends Record<string, any>>(auth: T): T;
56
+ /**
57
+ * Run a function with silgi context available to instrumented Better Auth API calls.
58
+ */
59
+ declare function withSilgiCtx<T>(ctx: Record<string, unknown>, fn: () => T): T;
60
+ //#endregion
61
+ export { SilgiTracingConfig, instrumentBetterAuth, silgiTracing, withSilgiCtx };
@@ -0,0 +1,332 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/integrations/better-auth/index.ts
3
+ /**
4
+ * Silgi + Better Auth tracing integration.
5
+ *
6
+ * Provides a Better Auth plugin factory that auto-traces all auth operations
7
+ * (sign-in, sign-up, OAuth, session management, etc.) into silgi analytics.
8
+ *
9
+ * The silgi request context is passed via `request.__silgiCtx`, set by
10
+ * the silgi auth handler before calling `auth.handler(request)`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { silgiTracing } from 'silgi/better-auth'
15
+ *
16
+ * const auth = betterAuth({
17
+ * plugins: [
18
+ * silgiTracing(), // auto-traces all auth operations
19
+ * ],
20
+ * })
21
+ * ```
22
+ */
23
+ const ctxStorage = new AsyncLocalStorage();
24
+ function matchOperation(path) {
25
+ const normalized = path.replace(/^\/+/, "");
26
+ if (normalized.endsWith("/sign-up/email") || normalized === "sign-up/email") return {
27
+ spanName: "auth.signup.email",
28
+ operation: "signup",
29
+ method: "email"
30
+ };
31
+ if (normalized.endsWith("/sign-in/email") || normalized === "sign-in/email") return {
32
+ spanName: "auth.signin.email",
33
+ operation: "signin",
34
+ method: "email"
35
+ };
36
+ if (normalized.endsWith("/sign-out") || normalized === "sign-out") return {
37
+ spanName: "auth.signout",
38
+ operation: "signout"
39
+ };
40
+ if (normalized.endsWith("/get-session") || normalized === "get-session") return {
41
+ spanName: "auth.get_session",
42
+ operation: "get_session"
43
+ };
44
+ if (normalized.endsWith("/update-user") || normalized === "update-user") return {
45
+ spanName: "auth.update_user",
46
+ operation: "update_user"
47
+ };
48
+ if (normalized.endsWith("/delete-user") || normalized === "delete-user") return {
49
+ spanName: "auth.delete_user",
50
+ operation: "delete_user"
51
+ };
52
+ if (normalized.endsWith("/change-password") || normalized === "change-password") return {
53
+ spanName: "auth.change_password",
54
+ operation: "change_password"
55
+ };
56
+ if (normalized.endsWith("/change-email") || normalized === "change-email") return {
57
+ spanName: "auth.change_email",
58
+ operation: "change_email"
59
+ };
60
+ if (normalized.endsWith("/verify-email") || normalized === "verify-email") return {
61
+ spanName: "auth.verify_email",
62
+ operation: "verify_email"
63
+ };
64
+ if (normalized.endsWith("/forget-password") || normalized === "forget-password") return {
65
+ spanName: "auth.forgot_password",
66
+ operation: "forgot_password"
67
+ };
68
+ if (normalized.endsWith("/reset-password") || normalized === "reset-password") return {
69
+ spanName: "auth.reset_password",
70
+ operation: "reset_password"
71
+ };
72
+ if (normalized.endsWith("/list-sessions") || normalized === "list-sessions") return {
73
+ spanName: "auth.list_sessions",
74
+ operation: "list_sessions"
75
+ };
76
+ if (normalized.endsWith("/revoke-session") || normalized === "revoke-session") return {
77
+ spanName: "auth.revoke_session",
78
+ operation: "revoke_session"
79
+ };
80
+ const callbackMatch = normalized.match(/\/callback\/([^/?]+)/);
81
+ if (callbackMatch) {
82
+ const provider = callbackMatch[1];
83
+ return {
84
+ spanName: `auth.oauth.callback.${provider}`,
85
+ operation: "oauth_callback",
86
+ method: "oauth",
87
+ provider
88
+ };
89
+ }
90
+ const signinMatch = normalized.match(/\/sign-in\/([^/?]+)$/);
91
+ if (signinMatch && signinMatch[1] !== "email") {
92
+ const provider = signinMatch[1];
93
+ return {
94
+ spanName: `auth.oauth.initiate.${provider}`,
95
+ operation: "oauth_initiate",
96
+ method: "oauth",
97
+ provider
98
+ };
99
+ }
100
+ const segments = normalized.split("/");
101
+ return {
102
+ spanName: `auth.${(segments[segments.length - 1] || "unknown").replace(/-/g, "_")}`,
103
+ operation: "unknown"
104
+ };
105
+ }
106
+ function round(n) {
107
+ return Math.round(n * 100) / 100;
108
+ }
109
+ function extractUserData(returned) {
110
+ const result = {};
111
+ if (!returned || typeof returned !== "object") return result;
112
+ const data = returned.data ?? returned;
113
+ if (data.user?.id) result.userId = String(data.user.id);
114
+ if (data.user?.email) result.userEmail = String(data.user.email);
115
+ if (data.session?.id) result.sessionId = String(data.session.id);
116
+ if (!result.userId && returned.id && returned.email) {
117
+ result.userId = String(returned.id);
118
+ result.userEmail = String(returned.email);
119
+ }
120
+ return result;
121
+ }
122
+ const requestMetas = /* @__PURE__ */ new WeakMap();
123
+ /**
124
+ * Creates a Better Auth plugin that auto-traces all auth operations
125
+ * into silgi analytics spans.
126
+ *
127
+ * @param config - Optional configuration
128
+ * @returns A Better Auth plugin (typed as `any` to avoid requiring better-auth types at build time)
129
+ */
130
+ function silgiTracing(config) {
131
+ const captureInput = config?.captureInput ?? true;
132
+ const captureOutput = config?.captureOutput ?? true;
133
+ return {
134
+ id: "silgi-tracing",
135
+ onRequest: async (request, _ctx) => {
136
+ try {
137
+ const path = new URL(request.url).pathname;
138
+ const match = matchOperation(path);
139
+ requestMetas.set(request, {
140
+ startTime: performance.now(),
141
+ path,
142
+ operation: match.operation,
143
+ method: match.method,
144
+ provider: match.provider,
145
+ spanName: match.spanName
146
+ });
147
+ } catch {}
148
+ },
149
+ hooks: { after: [{
150
+ matcher: () => true,
151
+ handler: (config?.createAuthMiddleware ?? ((fn) => fn))(async (ctx) => {
152
+ try {
153
+ const request = ctx.request;
154
+ if (!request) return;
155
+ const silgiCtx = request.__silgiCtx;
156
+ if (!silgiCtx) return;
157
+ const reqTrace = silgiCtx.__analyticsTrace;
158
+ if (!reqTrace) return;
159
+ const meta = requestMetas.get(request);
160
+ requestMetas.delete(request);
161
+ const path = ctx.path || "";
162
+ const match = meta ? {
163
+ spanName: meta.spanName,
164
+ operation: meta.operation,
165
+ method: meta.method,
166
+ provider: meta.provider
167
+ } : matchOperation(path);
168
+ const startTime = meta?.startTime ?? performance.now();
169
+ const durationMs = round(performance.now() - startTime);
170
+ const returned = ctx.context?.returned;
171
+ const newSession = ctx.context?.newSession;
172
+ const userData = extractUserData(returned);
173
+ if (!userData.userId && newSession?.user?.id) userData.userId = String(newSession.user.id);
174
+ if (!userData.userEmail && newSession?.user?.email) userData.userEmail = String(newSession.user.email);
175
+ if (!userData.sessionId && newSession?.session?.id) userData.sessionId = String(newSession.session.id);
176
+ const success = !returned?.error && !ctx.context?.error;
177
+ const attributes = {
178
+ "auth.operation": match.operation,
179
+ "auth.success": success
180
+ };
181
+ if (match.method) attributes["auth.method"] = match.method;
182
+ if (match.provider) attributes["auth.provider"] = match.provider;
183
+ if (userData.userId) attributes["user.id"] = userData.userId;
184
+ if (userData.userEmail) attributes["user.email"] = userData.userEmail;
185
+ if (userData.sessionId) attributes["session.id"] = userData.sessionId;
186
+ const span = {
187
+ name: match.spanName,
188
+ kind: "http",
189
+ durationMs,
190
+ startOffsetMs: round(startTime - reqTrace.t0),
191
+ attributes
192
+ };
193
+ if (captureInput && ctx.body) span.input = ctx.body;
194
+ if (captureOutput && returned && typeof returned === "object") span.output = returned;
195
+ if (!success && returned?.error) span.error = typeof returned.error === "string" ? returned.error : returned.error?.message ?? "error";
196
+ reqTrace.spans.push(span);
197
+ if (captureInput && ctx.body) reqTrace.procedureInput = ctx.body;
198
+ if (captureOutput && returned && typeof returned === "object") reqTrace.procedureOutput = returned;
199
+ } catch {}
200
+ })
201
+ }] }
202
+ };
203
+ }
204
+ const API_METHOD_METADATA = {
205
+ getSession: { operation: "get_session" },
206
+ signOut: { operation: "signout" },
207
+ signInEmail: {
208
+ operation: "signin",
209
+ method: "email"
210
+ },
211
+ signUpEmail: {
212
+ operation: "signup",
213
+ method: "email"
214
+ },
215
+ signInSocial: {
216
+ operation: "signin",
217
+ method: "oauth"
218
+ },
219
+ callbackOAuth: {
220
+ operation: "oauth_callback",
221
+ method: "oauth"
222
+ },
223
+ linkSocialAccount: {
224
+ operation: "link_social_account",
225
+ method: "oauth"
226
+ },
227
+ unlinkAccount: { operation: "unlink_account" },
228
+ listUserAccounts: { operation: "list_user_accounts" },
229
+ updateUser: { operation: "update_user" },
230
+ deleteUser: { operation: "delete_user" },
231
+ changePassword: { operation: "change_password" },
232
+ setPassword: { operation: "set_password" },
233
+ changeEmail: { operation: "change_email" },
234
+ verifyEmail: { operation: "verify_email" },
235
+ sendVerificationEmail: { operation: "send_verification_email" },
236
+ forgetPassword: { operation: "forget_password" },
237
+ resetPassword: { operation: "reset_password" },
238
+ listSessions: { operation: "list_sessions" },
239
+ revokeSession: { operation: "revoke_session" },
240
+ revokeSessions: { operation: "revoke_sessions" },
241
+ revokeOtherSessions: { operation: "revoke_other_sessions" },
242
+ refreshToken: { operation: "refresh_token" },
243
+ getAccessToken: { operation: "get_access_token" }
244
+ };
245
+ const AUTH_INSTRUMENTED = "__silgiBetterAuthInstrumented";
246
+ /**
247
+ * Instrument a Better Auth instance to trace all `auth.api.*` method calls.
248
+ * Works with `withSilgiCtx` — programmatic calls from background jobs,
249
+ * server-side session fetches etc. are traced when context is available.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * import { instrumentBetterAuth, withSilgiCtx } from 'silgi/better-auth'
254
+ *
255
+ * const auth = instrumentBetterAuth(betterAuth({ ... }))
256
+ *
257
+ * // In a silgi procedure:
258
+ * const me = s.$resolve(async ({ ctx }) => {
259
+ * return withSilgiCtx(ctx, () => auth.api.getSession({ headers: ctx.headers }))
260
+ * })
261
+ * ```
262
+ */
263
+ function instrumentBetterAuth(auth) {
264
+ if (!auth || auth[AUTH_INSTRUMENTED]) return auth;
265
+ const api = auth.api;
266
+ if (!api || typeof api !== "object") return auth;
267
+ const instrumented = /* @__PURE__ */ new Set();
268
+ for (const [methodName, metadata] of Object.entries(API_METHOD_METADATA)) if (typeof api[methodName] === "function") {
269
+ api[methodName] = wrapApiMethod(api[methodName], metadata.operation, metadata.method);
270
+ instrumented.add(methodName);
271
+ }
272
+ for (const key of Object.keys(api)) if (typeof api[key] === "function" && !instrumented.has(key) && !key.startsWith("$") && !key.startsWith("_")) {
273
+ const operation = key.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
274
+ api[key] = wrapApiMethod(api[key], operation);
275
+ instrumented.add(key);
276
+ }
277
+ auth[AUTH_INSTRUMENTED] = true;
278
+ return auth;
279
+ }
280
+ /**
281
+ * Run a function with silgi context available to instrumented Better Auth API calls.
282
+ */
283
+ function withSilgiCtx(ctx, fn) {
284
+ return ctxStorage.run(ctx, fn);
285
+ }
286
+ function wrapApiMethod(originalFn, operation, method) {
287
+ return async function instrumented(...args) {
288
+ const reqTrace = ctxStorage.getStore()?.__analyticsTrace;
289
+ if (!reqTrace) return originalFn.apply(this, args);
290
+ const spanName = `auth.api.${operation}`;
291
+ const start = performance.now();
292
+ const attributes = {
293
+ "auth.operation": operation,
294
+ "auth.success": true
295
+ };
296
+ if (method) attributes["auth.method"] = method;
297
+ try {
298
+ const result = await originalFn.apply(this, args);
299
+ const data = result?.data ?? result;
300
+ if (data?.user?.id) attributes["user.id"] = String(data.user.id);
301
+ if (data?.user?.email) attributes["user.email"] = String(data.user.email);
302
+ if (data?.session?.id) attributes["session.id"] = String(data.session.id);
303
+ if (result?.error) {
304
+ attributes["auth.success"] = false;
305
+ attributes["auth.error"] = typeof result.error === "string" ? result.error : result.error?.message ?? "unknown";
306
+ }
307
+ reqTrace.spans.push({
308
+ name: spanName,
309
+ kind: "http",
310
+ durationMs: round(performance.now() - start),
311
+ startOffsetMs: round(start - reqTrace.t0),
312
+ attributes,
313
+ output: result && typeof result === "object" ? result : void 0
314
+ });
315
+ return result;
316
+ } catch (error) {
317
+ attributes["auth.success"] = false;
318
+ attributes["auth.error"] = error instanceof Error ? error.message : String(error);
319
+ reqTrace.spans.push({
320
+ name: spanName,
321
+ kind: "http",
322
+ durationMs: round(performance.now() - start),
323
+ startOffsetMs: round(start - reqTrace.t0),
324
+ attributes,
325
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
326
+ });
327
+ throw error;
328
+ }
329
+ };
330
+ }
331
+ //#endregion
332
+ export { instrumentBetterAuth, silgiTracing, withSilgiCtx };
@@ -0,0 +1,27 @@
1
+ //#region src/integrations/drizzle/index.d.ts
2
+ interface InstrumentDrizzleConfig {
3
+ /** Logical database name (e.g. 'auth', 'ecommerce') */
4
+ dbName?: string;
5
+ /** Database system identifier. Default: 'postgresql' */
6
+ dbSystem?: string;
7
+ /** Capture SQL query text in spans. Default: true */
8
+ captureQueryText?: boolean;
9
+ /** Max query text length before truncation. Default: 1000 */
10
+ maxQueryTextLength?: number;
11
+ /** Database host */
12
+ peerName?: string;
13
+ /** Database port */
14
+ peerPort?: number;
15
+ }
16
+ /**
17
+ * Instrument a Drizzle db instance to record query spans in silgi analytics.
18
+ * Returns the same db instance (mutated). Safe to call multiple times.
19
+ */
20
+ declare function instrumentDrizzle<T extends Record<string, any>>(db: T, config?: InstrumentDrizzleConfig): T;
21
+ /**
22
+ * Run a function with silgi context available to instrumented Drizzle instances.
23
+ * All Drizzle queries inside `fn` will be recorded as trace spans.
24
+ */
25
+ declare function withSilgiCtx<T>(ctx: Record<string, unknown>, fn: () => T): T;
26
+ //#endregion
27
+ export { InstrumentDrizzleConfig, instrumentDrizzle, withSilgiCtx };