From a3f707a361fcb49c41ba187ff3faa71ce2a64411 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Wed, 22 Apr 2026 19:07:17 +0800 Subject: [PATCH] feat: instrument tool_used, pipeline_executed, ai_bundle_action events --- apps/api/src/routes/features.ts | 17 ++++++++++++- apps/api/src/routes/pipeline.ts | 37 ++++++++++++++++++++++++++++- apps/api/src/routes/tool-factory.ts | 19 ++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/apps/api/src/routes/features.ts b/apps/api/src/routes/features.ts index 439cc747..1dcedf2a 100644 --- a/apps/api/src/routes/features.ts +++ b/apps/api/src/routes/features.ts @@ -12,8 +12,9 @@ import crypto from "node:crypto"; import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { shutdownDispatcher } from "@ashim/ai"; -import { FEATURE_BUNDLES } from "@ashim/shared"; +import { ANALYTICS_EVENTS, FEATURE_BUNDLES } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { trackEvent } from "../lib/analytics.js"; import { acquireInstallLock, getAiDir, @@ -133,6 +134,9 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise const manifestPath = getManifestPath(); const modelsDir = getModelsDir(); + const installStartTime = Date.now(); + const reqRef = request; + const child = spawn(pythonPath, [scriptPath, bundleId, manifestPath, modelsDir], { stdio: ["ignore", "pipe", "pipe"], env: { @@ -192,6 +196,11 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise shutdownDispatcher(); setInstallProgress(null, null, null); updateSingleFileProgress({ jobId, phase: "complete", percent: 100, stage: "Complete" }); + trackEvent(reqRef, ANALYTICS_EVENTS.AI_BUNDLE_ACTION, { + bundle_id: bundleId, + action: "installed", + duration_ms: Date.now() - installStartTime, + }); } else { const errorDetail = lastStderrLines.filter((l) => !l.startsWith("{")).join("\n") || stdoutBuffer.trim(); @@ -264,6 +273,12 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise markUninstalled(bundleId); shutdownDispatcher(); + trackEvent(request, ANALYTICS_EVENTS.AI_BUNDLE_ACTION, { + bundle_id: bundleId, + action: "uninstalled", + duration_ms: 0, + }); + return reply.send({ ok: true }); }, ); diff --git a/apps/api/src/routes/pipeline.ts b/apps/api/src/routes/pipeline.ts index e4e895e9..beb6e788 100644 --- a/apps/api/src/routes/pipeline.ts +++ b/apps/api/src/routes/pipeline.ts @@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; +import { ANALYTICS_EVENTS, getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import archiver from "archiver"; import { eq } from "drizzle-orm"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; @@ -17,6 +17,7 @@ import PQueue from "p-queue"; import { z } from "zod"; import { env } from "../config.js"; import { db, schema } from "../db/index.js"; +import { trackEvent } from "../lib/analytics.js"; import { autoOrient } from "../lib/auto-orient.js"; import { resolveConcurrency } from "../lib/env.js"; import { formatZodErrors } from "../lib/errors.js"; @@ -204,6 +205,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise = []; @@ -244,6 +246,13 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise s.toolId), + is_batch: false, + duration_ms: Date.now() - startTime, + status: "failed", + }); return reply.status(422).send({ error: message, completedSteps: stepResults, @@ -260,6 +269,14 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise s.toolId), + is_batch: false, + duration_ms: Date.now() - startTime, + status: "completed", + }); + return reply.send({ jobId, downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(currentFilename)}`, @@ -508,6 +525,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise s.toolId), + is_batch: true, + file_count: files.length, + duration_ms: Date.now() - batchStartTime, + status: "failed", + }); return reply.status(422).send({ error: "All files failed processing", errors: progress.errors, }); } + trackEvent(request, ANALYTICS_EVENTS.PIPELINE_EXECUTED, { + step_count: pipeline.steps.length, + tool_ids: pipeline.steps.map((s: { toolId: string }) => s.toolId), + is_batch: true, + file_count: files.length, + duration_ms: Date.now() - batchStartTime, + status: "completed", + }); + // ── Stream ZIP response ────────────────────────────────────────── reply.hijack(); reply.raw.writeHead(200, { diff --git a/apps/api/src/routes/tool-factory.ts b/apps/api/src/routes/tool-factory.ts index 45da7a95..78830bd8 100644 --- a/apps/api/src/routes/tool-factory.ts +++ b/apps/api/src/routes/tool-factory.ts @@ -1,12 +1,13 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { extname, join } from "node:path"; -import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; +import { ANALYTICS_EVENTS, getBundleForTool, TOOL_BUNDLE_MAP, TOOLS } from "@ashim/shared"; import { eq } from "drizzle-orm"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import type { z } from "zod"; import { db, schema } from "../db/index.js"; +import { trackEvent } from "../lib/analytics.js"; import { autoOrient } from "../lib/auto-orient.js"; import { formatZodErrors } from "../lib/errors.js"; import { isToolInstalled } from "../lib/feature-status.js"; @@ -242,6 +243,7 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig } // Process the image (worker thread or main thread) + const startTime = Date.now(); try { let result: { buffer: Buffer; filename: string; contentType: string }; @@ -381,6 +383,14 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig } } + trackEvent(request, ANALYTICS_EVENTS.TOOL_USED, { + tool_id: config.toolId, + status: "completed", + duration_ms: Date.now() - startTime, + category: TOOLS.find((t) => t.id === config.toolId)?.category ?? "unknown", + is_ai_tool: getBundleForTool(config.toolId) !== null, + }); + return reply.send({ jobId, downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(result.filename)}`, @@ -393,6 +403,13 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig // Catch Sharp / processing errors and return a clean API error const message = err instanceof Error ? err.message : "Image processing failed"; request.log.error({ err, toolId: config.toolId }, "Tool processing failed"); + trackEvent(request, ANALYTICS_EVENTS.TOOL_USED, { + tool_id: config.toolId, + status: "failed", + duration_ms: Date.now() - startTime, + category: TOOLS.find((t) => t.id === config.toolId)?.category ?? "unknown", + is_ai_tool: getBundleForTool(config.toolId) !== null, + }); return reply.status(422).send({ error: "Processing failed", details: message,