From e55628def8a631488bb906d6ef9f54d666a3396e Mon Sep 17 00:00:00 2001 From: Brad Groux Date: Mon, 26 Jan 2026 12:15:02 -0600 Subject: [PATCH] feat(telemetry): emit task lifecycle events (US-802) TaskService now emits telemetry events for: - task.created: when a new task is created - task.status_changed: when task status changes (with previousStatus) - task.archived: when a task is archived - task.restored: when a task is restored from archive All events include taskId, project, and current status. --- server/src/services/task-service.ts | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/server/src/services/task-service.ts b/server/src/services/task-service.ts index e302d4e3..959c02fd 100644 --- a/server/src/services/task-service.ts +++ b/server/src/services/task-service.ts @@ -2,7 +2,8 @@ import fs from 'fs/promises'; import path from 'path'; import matter from 'gray-matter'; import { nanoid } from 'nanoid'; -import type { Task, CreateTaskInput, UpdateTaskInput, ReviewComment, Subtask } from '@veritas-kanban/shared'; +import type { Task, CreateTaskInput, UpdateTaskInput, ReviewComment, Subtask, TaskTelemetryEvent } from '@veritas-kanban/shared'; +import { getTelemetryService, type TelemetryService } from './telemetry-service.js'; // Simple slug function to avoid CJS/ESM issues with slugify function makeSlug(text: string): string { @@ -21,15 +22,18 @@ const DEFAULT_ARCHIVE_DIR = path.join(DEFAULT_PROJECT_ROOT, 'tasks', 'archive'); export interface TaskServiceOptions { tasksDir?: string; archiveDir?: string; + telemetryService?: TelemetryService; } export class TaskService { private tasksDir: string; private archiveDir: string; + private telemetry: TelemetryService; constructor(options: TaskServiceOptions = {}) { this.tasksDir = options.tasksDir || DEFAULT_TASKS_DIR; this.archiveDir = options.archiveDir || DEFAULT_ARCHIVE_DIR; + this.telemetry = options.telemetryService || getTelemetryService(); this.ensureDirectories(); } @@ -152,6 +156,14 @@ export class TaskService { await fs.writeFile(filepath, content, 'utf-8'); + // Emit telemetry event + await this.telemetry.emit({ + type: 'task.created', + taskId: task.id, + project: task.project, + status: task.status, + }); + return task; } @@ -159,6 +171,10 @@ export class TaskService { const task = await this.getTask(id); if (!task) return null; + // Track if status changed for telemetry + const previousStatus = task.status; + const statusChanged = input.status !== undefined && input.status !== previousStatus; + // Handle git field separately to merge properly const { git: gitUpdate, ...restInput } = input; @@ -182,6 +198,17 @@ export class TaskService { await fs.writeFile(filepath, content, 'utf-8'); + // Emit telemetry event if status changed + if (statusChanged) { + await this.telemetry.emit({ + type: 'task.status_changed', + taskId: updatedTask.id, + project: updatedTask.project, + status: updatedTask.status, + previousStatus, + }); + } + return updatedTask; } @@ -205,6 +232,14 @@ export class TaskService { await fs.rename(sourcePath, destPath); + // Emit telemetry event + await this.telemetry.emit({ + type: 'task.archived', + taskId: task.id, + project: task.project, + status: task.status, + }); + return true; } @@ -254,6 +289,14 @@ export class TaskService { const content = this.taskToMarkdown(restoredTask); await fs.writeFile(destPath, content, 'utf-8'); + // Emit telemetry event + await this.telemetry.emit({ + type: 'task.restored', + taskId: restoredTask.id, + project: restoredTask.project, + status: restoredTask.status, + }); + return restoredTask; }