Skip to content

Fix Double Counting Bug - Detailed Implementation Plan ​

Progress Summary ​

  • ✅ Phase 1: Remove Dead Code - COMPLETED 2024-08-29
  • ✅ Phase 2: Fix Resume Check Order - COMPLETED 2024-08-29
  • ✅ Phase 3: Create AnalyticsRecorder Service - COMPLETED
  • ✅ Phase 4: Remove Tracking from generateLLMResponse - COMPLETED
  • ✅ Phase 5: Update All Routes - COMPLETED
  • ⏳ Phase 6: Clean Database Functions - PENDING
  • ⏳ Phase 7: Update TypeScript Types - PENDING
  • ⏳ Phase 8: Final Testing - PENDING

Overview ​

This plan addresses the double counting bug where each job analysis is counted twice in the database, along with related architectural improvements.

Key Issues to Fix ​

  1. Double counting bug: Both Stage 1 and Stage 2 of job analysis use 'job_analysis' operation type
  2. Inefficient resume check: Happens between stages instead of before Stage 1
  3. Dead code: Unused compatibility endpoint and resume-analyzer.ts (~500 lines)
  4. Database functions with business logic: Duplicate CASE statements in multiple functions
  5. Tight coupling: update_user_usage calls update_daily_stats directly

Phase 1: Remove Dead Code ✅ COMPLETED ​

Files to Delete ​

  1. /apps/web/src/app/api/jobs/[id]/compatibility/route.ts - Unused API endpoint ✅
  2. /apps/web/src/lib/resume-analyzer.ts - Only used by dead endpoint ✅

Verification Steps ​

bash
# Check for any remaining references
grep -r "compatibility/route" apps/web/src
grep -r "resume-analyzer" apps/web/src
grep -r "performFullResumeAnalysis" apps/web/src

Testing After Phase 1 ​

bash
npm run build --workspace=apps/web  ✅ Passed
npm run lint --workspace=apps/web   ✅ No warnings or errors
npm run type-check --workspace=apps/web  ✅ Passed

Execution Results (2024-08-29) ​

  • Files Deleted:
    • /apps/web/src/app/api/jobs/[id]/compatibility/route.ts (303 lines)
    • /apps/web/src/lib/resume-analyzer.ts (459 lines)
    • Empty directory /apps/web/src/app/api/jobs/[id]/compatibility/
  • Total Lines Removed: ~762 lines
  • Verification: All references checked, only found in deleted files and plan doc
  • Build Status: ✅ Success - compiled in 5.0s
  • Lint Status: ✅ No ESLint warnings or errors
  • TypeScript: ✅ No type errors

Phase 2: Fix Resume Check Order in /api/jobs/analyze ✅ COMPLETED ​

Current Code (Inefficient) ​

typescript
// apps/web/src/app/api/jobs/analyze/route.ts (lines ~350-430)

// STAGE 1: Generate structured job data
const systemPrompt1 = getJobStructuringPrompt();
// ... prepare prompts ...

llmResponse1 = await generateLLMResponse(
  {
    systemPrompt: systemPrompt1,
    userPrompt: userPrompt1,
    temperature: 0.1,
    maxTokens: GEMINI_MAX_OUTPUT_TOKENS,
  },
  model,
  userId,
  supabase,
  USAGE_OPERATION_TYPES.JOB_ANALYSIS  // ← First count
);

// Parse Stage 1 response
structuredJob = parseLLMJsonResponse(llmResponse1);

// STAGE 2: Generate resume compatibility analysis
if (resumeContent) {  // ← Resume check happens AFTER Stage 1
  const systemPrompt2 = getResumeCompatibilityPrompt();
  // ... prepare prompts ...
  
  llmResponse2 = await generateLLMResponse(
    {
      systemPrompt: systemPrompt2,
      userPrompt: userPrompt2,
      temperature: 0.1,
      maxTokens: GEMINI_MAX_OUTPUT_TOKENS,
    },
    model,
    userId,
    supabase,
    USAGE_OPERATION_TYPES.JOB_ANALYSIS  // ← Second count (BUG!)
  );
  
  compatibilityAnalysis = parseLLMJsonResponse(llmResponse2);
}

New Code (Efficient & Fixed) ​

typescript
// apps/web/src/app/api/jobs/analyze/route.ts

// Check for resume FIRST
const hasResume = !!resumeContent;

// STAGE 1: Generate structured job data
const systemPrompt1 = getJobStructuringPrompt(hasResume); // ← Pass flag to adjust prompt
// ... prepare prompts ...

llmResponse1 = await generateLLMResponse(
  {
    systemPrompt: systemPrompt1,
    userPrompt: userPrompt1,
    temperature: 0.1,
    maxTokens: GEMINI_MAX_OUTPUT_TOKENS,
  },
  model,
  userId,
  supabase,
  null  // ← No tracking here
);

// Parse Stage 1 response
structuredJob = parseLLMJsonResponse(llmResponse1);

// STAGE 2: Generate resume compatibility analysis (only if resume exists)
if (hasResume) {
  const systemPrompt2 = getResumeCompatibilityPrompt();
  // ... prepare prompts ...
  
  llmResponse2 = await generateLLMResponse(
    {
      systemPrompt: systemPrompt2,
      userPrompt: userPrompt2,
      temperature: 0.1,
      maxTokens: GEMINI_MAX_OUTPUT_TOKENS,
    },
    model,
    userId,
    supabase,
    null  // ← No tracking here either
  );
  
  compatibilityAnalysis = parseLLMJsonResponse(llmResponse2);
}

// Track ONCE after both stages complete
const totalCost = llmResponse1.usage.estimatedCost + 
                  (llmResponse2?.usage?.estimatedCost || 0);

await analyticsRecorder.trackJobAnalysis(userId, totalCost, {
  hasResume,
  stage1Tokens: llmResponse1.usage.totalTokens,
  stage2Tokens: llmResponse2?.usage?.totalTokens || 0,
});

Testing After Phase 2 ​

bash
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web
# Test the API endpoint manually

Execution Results (2024-08-29) ​

  • Key Changes:

    • Moved resume check from line 208 to line 103 (right after auth)
    • IMPORTANT: Removed resume sanitization - resume now used as-is from database
    • Deleted duplicate Supabase client creation and resume fetching (lines 245-291)
    • Resume content stored as: const resumeContent = userProfile.resume_markdown; (NO sanitization)
  • Benefits:

    • Fail fast if no resume exists (skip expensive operations)
    • Resume markdown preserved exactly (quotes, apostrophes, all formatting intact)
    • Removed ~45 lines of duplicate code
  • Verification:

    • Build Status: ✅ Success
    • Lint Status: ✅ No ESLint warnings or errors
    • TypeScript: ✅ No type errors
  • Note: The plan originally didn't mention removing sanitization, but we discovered during implementation that sanitizing the resume was harmful (removed quotes, apostrophes, and HTML-like content from markdown). Per user directive, resume is now used exactly as stored in the database.


Phase 3: Create AnalyticsRecorder Service ​

New File: /apps/web/src/lib/analytics-recorder.ts ​

typescript
/**
 * AnalyticsRecorder - Centralized service for tracking operations and usage
 * 
 * This service decouples LLM API calls from usage tracking, providing
 * explicit control over what gets tracked and when.
 */

import { SupabaseClient } from '@supabase/supabase-js';
import { errorLog, infoLog } from './logger';

/**
 * Operation types that increment counts in the database
 */
export type TrackedOperation = 
  | 'job_analysis'           // Complete job analysis (both stages)
  | 'resume_customization'    // Resume customization for a job
  | 'cover_letter'           // Cover letter generation
  | 'interview_prep'         // Interview preparation
  | 'saved_job'              // Job saved (auto-tracked by DB trigger)
  | 'google_drive'           // Google Drive operations
  | 'google_docs';           // Google Docs operations

/**
 * Internal operation types for granular tracking (cost only, no count increment)
 * These leverage the ELSE clause in the database function
 */
export type InternalOperation = 
  | 'job_structuring'        // Stage 1 of job analysis
  | 'compatibility_check'    // Stage 2 of job analysis
  | 'llm_internal';          // Generic internal LLM operation

export type OperationType = TrackedOperation | InternalOperation;

export interface TrackingData {
  userId: string;
  cost: number;
  operation: OperationType;
  metadata?: Record<string, unknown>;
}

export interface OperationResult {
  success: boolean;
  cost: number;
  error?: string;
}

/**
 * Main AnalyticsRecorder class
 */
export class AnalyticsRecorder {
  constructor(private supabase: SupabaseClient) {}

  /**
   * Track a complete user operation with cost
   */
  async trackOperation(data: TrackingData): Promise<OperationResult> {
    const maxRetries = 3;
    let lastError: Error | unknown;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        if (attempt > 1) {
          const delay = 100 * Math.pow(2, attempt - 1);
          await new Promise(resolve => setTimeout(resolve, delay));
        }

        const currentDate = new Date().toISOString().split('T')[0];

        // Call the database function
        const { error } = await this.supabase.rpc('update_user_usage', {
          p_user_id: data.userId,
          p_date: currentDate,
          p_cost: data.cost,
          p_operation_type: data.operation,
        });

        if (error) {
          lastError = error;
          errorLog('AnalyticsRecorder', 'Failed to track operation', { attempt, error });
          continue;
        }

        if (process.env.NODE_ENV === 'development') {
          infoLog('AnalyticsRecorder', 'Operation tracked successfully', {
            userId: data.userId,
            operation: data.operation,
            cost: data.cost,
            metadata: data.metadata,
          });
        }

        return { success: true, cost: data.cost };
      } catch (error) {
        lastError = error;
        errorLog('AnalyticsRecorder', 'Failed to track operation', { attempt, error });
      }
    }

    const errorMessage = `Failed to track operation after ${maxRetries} attempts`;
    errorLog('AnalyticsRecorder', errorMessage, lastError);
    
    return {
      success: false,
      cost: data.cost,
      error: errorMessage,
    };
  }

  /**
   * Track a complete job analysis (both stages)
   */
  async trackJobAnalysis(
    userId: string,
    totalCost: number,
    metadata?: Record<string, unknown>
  ): Promise<OperationResult> {
    return this.trackOperation({
      userId,
      cost: totalCost,
      operation: 'job_analysis',
      metadata,
    });
  }

  /**
   * Track internal operations (cost only, no count increment)
   */
  async trackInternalOperation(
    userId: string,
    cost: number,
    operation: InternalOperation,
    metadata?: Record<string, unknown>
  ): Promise<OperationResult> {
    return this.trackOperation({
      userId,
      cost,
      operation,
      metadata,
    });
  }

  // Additional convenience methods
  async trackResumeCustomization(userId: string, cost: number, metadata?: Record<string, unknown>): Promise<OperationResult> {
    return this.trackOperation({ userId, cost, operation: 'resume_customization', metadata });
  }

  async trackCoverLetter(userId: string, cost: number, metadata?: Record<string, unknown>): Promise<OperationResult> {
    return this.trackOperation({ userId, cost, operation: 'cover_letter', metadata });
  }

  async trackInterviewPrep(userId: string, cost: number, metadata?: Record<string, unknown>): Promise<OperationResult> {
    return this.trackOperation({ userId, cost, operation: 'interview_prep', metadata });
  }
}

/**
 * Factory function to create AnalyticsRecorder instance
 */
export function createAnalyticsRecorder(supabase: SupabaseClient): AnalyticsRecorder {
  return new AnalyticsRecorder(supabase);
}

Testing After Phase 3 ​

bash
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web

Phase 4: Remove Tracking from generateLLMResponse ​

Current Code (with automatic tracking) ​

typescript
// apps/web/src/lib/llm-providers.ts (lines ~252-371)

export async function generateLLMResponse(
  params: {
    systemPrompt: string;
    userPrompt: string;
    temperature?: number;
    maxTokens?: number;
  },
  model: LLMModel,
  userId: string,
  supabase: SupabaseClient,
  operationType?: string
): Promise<LLMResponse<string>> {
  // ... LLM call logic ...
  
  // Automatic tracking (REMOVE THIS)
  if (userId && supabase && operationType) {
    await logLLMUsage(
      supabase,
      userId,
      model,
      usage,
      operationType
    );
  }
  
  return response;
}

New Code (no automatic tracking) ​

typescript
// apps/web/src/lib/llm-providers.ts

export async function generateLLMResponse(
  params: {
    systemPrompt: string;
    userPrompt: string;
    temperature?: number;
    maxTokens?: number;
  },
  model: LLMModel,
  userId?: string,  // ← Now optional
  supabase?: SupabaseClient,  // ← Now optional
  operationType?: string  // ← Deprecated, will be removed
): Promise<LLMResponse<string>> {
  // ... LLM call logic ...
  
  // NO AUTOMATIC TRACKING - caller handles this
  
  // Return response with usage data
  return {
    data: completionText,
    usage: {
      promptTokens,
      completionTokens,
      totalTokens,
      estimatedCost,
    },
    model: model.modelId,
    provider: model.provider,
    timestamp: Date.now(),
  };
}

/**
 * @deprecated Use AnalyticsRecorder instead
 */
export async function logLLMUsage(
  supabase: SupabaseClient,
  userId: string,
  model: LLMModel,
  usage: LLMUsage,
  operation: string
): Promise<void> {
  // Mark as deprecated
  console.warn('logLLMUsage is deprecated. Use AnalyticsRecorder instead.');
  
  // Delegate to AnalyticsRecorder for backward compatibility
  const { createAnalyticsRecorder } = await import('./analytics-recorder');
  const recorder = createAnalyticsRecorder(supabase);
  
  await recorder.trackOperation({
    userId,
    cost: usage.estimatedCost,
    operation: operation as any,
    metadata: {
      model: model.modelId,
      tokens: usage.totalTokens,
    },
  });
}

Testing After Phase 4 ​

bash
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web

Phase 5: Update All Routes ​

5.1: Fix /api/jobs/analyze ​

Already shown in Phase 2 above.

5.2: Update /api/jobs/structure ​

Before ​

typescript
// apps/web/src/app/api/jobs/structure/route.ts
const llmResponse = await generateLLMResponse(
  { systemPrompt, userPrompt, temperature: 0.1, maxTokens: 16384 },
  model,
  userId,
  supabase,
  'job_analysis'  // ← This increments count
);

After ​

typescript
// apps/web/src/app/api/jobs/structure/route.ts
import { createAnalyticsRecorder } from '@/lib/analytics-recorder';

const llmResponse = await generateLLMResponse(
  { systemPrompt, userPrompt, temperature: 0.1, maxTokens: 16384 },
  model
  // No userId, supabase, or operation type
);

// Track as internal operation (cost only, no count)
const recorder = createAnalyticsRecorder(supabase);
await recorder.trackInternalOperation(
  userId,
  llmResponse.usage.estimatedCost,
  'job_structuring',
  { source: 'structure_endpoint' }
);

5.3: Update /api/jobs/[id]/resume/customize ​

Before ​

typescript
const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model,
  userId,
  supabase,
  USAGE_OPERATION_TYPES.RESUME_CUSTOMIZATION
);

After ​

typescript
import { createAnalyticsRecorder } from '@/lib/analytics-recorder';

const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model
);

const recorder = createAnalyticsRecorder(supabase);
await recorder.trackResumeCustomization(
  userId,
  response.usage.estimatedCost,
  { jobId: resolvedParams.id }
);

5.4: Update /api/jobs/[id]/cover-letter ​

Before ​

typescript
const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model,
  userId,
  supabase,
  USAGE_OPERATION_TYPES.COVER_LETTER
);

After ​

typescript
import { createAnalyticsRecorder } from '@/lib/analytics-recorder';

const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model
);

const recorder = createAnalyticsRecorder(supabase);
await recorder.trackCoverLetter(
  userId,
  response.usage.estimatedCost,
  { jobId: resolvedParams.id }
);

5.5: Update /api/jobs/[id]/interview-prep ​

Before ​

typescript
const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model,
  userId,
  supabase,
  USAGE_OPERATION_TYPES.INTERVIEW_PREP
);

After ​

typescript
import { createAnalyticsRecorder } from '@/lib/analytics-recorder';

const response = await generateLLMResponse(
  { systemPrompt, userPrompt, maxTokens: GEMINI_MAX_OUTPUT_TOKENS },
  model
);

const recorder = createAnalyticsRecorder(supabase);
await recorder.trackInterviewPrep(
  userId,
  response.usage.estimatedCost,
  { jobId: resolvedParams.id }
);

Testing After Phase 5 ​

bash
npm run build --workspace=apps/web
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web
# Manual API testing for each endpoint

Phase 6: Clean Database Functions (Option B - Clean Architecture) ​

6.1: Create New Simplified Functions ​

sql
-- Migration: Create clean tracking functions

-- Simple cost tracking function (no business logic)
CREATE OR REPLACE FUNCTION update_usage_cost(
  p_user_id UUID,
  p_date DATE,
  p_cost NUMERIC,
  p_column_name TEXT  -- Dynamic column name
)
RETURNS VOID AS $$
BEGIN
  -- Dynamically update the specified cost column
  EXECUTE format(
    'UPDATE user_usage_summary 
     SET %I = %I + $1, 
         total_cost_usd = total_cost_usd + $1,
         updated_at = NOW()
     WHERE user_id = $2 
       AND $3 >= period_start 
       AND $3 <= period_end',
    p_column_name, p_column_name
  ) USING p_cost, p_user_id, p_date;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Simple count tracking function (no business logic)
CREATE OR REPLACE FUNCTION update_usage_count(
  p_user_id UUID,
  p_date DATE,
  p_column_name TEXT  -- Dynamic column name
)
RETURNS VOID AS $$
BEGIN
  -- Dynamically update the specified count column
  EXECUTE format(
    'UPDATE user_usage_summary 
     SET %I = %I + 1,
         updated_at = NOW()
     WHERE user_id = $2 
       AND $3 >= period_start 
       AND $3 <= period_end',
    p_column_name
  ) USING p_user_id, p_date;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Simple daily stats function (no business logic)
CREATE OR REPLACE FUNCTION update_daily_count(
  p_user_id UUID,
  p_date DATE,
  p_column_name TEXT
)
RETURNS VOID AS $$
BEGIN
  -- Ensure row exists
  INSERT INTO user_daily_stats (user_id, date, updated_at)
  VALUES (p_user_id, p_date, NOW())
  ON CONFLICT (user_id, date) DO NOTHING;
  
  -- Update the count
  EXECUTE format(
    'UPDATE user_daily_stats 
     SET %I = %I + 1,
         updated_at = NOW()
     WHERE user_id = $1 AND date = $2',
    p_column_name, p_column_name
  ) USING p_user_id, p_date;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

6.2: Update AnalyticsRecorder to Use New Functions ​

typescript
// apps/web/src/lib/analytics-recorder.ts

async trackOperation(data: TrackingData): Promise<OperationResult> {
  // ... retry logic ...
  
  const currentDate = new Date().toISOString().split('T')[0];
  
  // Map operation types to database columns
  const columnMapping: Record<TrackedOperation, { costColumn: string; countColumn: string }> = {
    'job_analysis': { 
      costColumn: 'job_analysis_cost_usd', 
      countColumn: 'job_analysis_count' 
    },
    'resume_customization': { 
      costColumn: 'resume_customization_cost_usd', 
      countColumn: 'resume_customization_count' 
    },
    'cover_letter': { 
      costColumn: 'cover_letter_cost_usd', 
      countColumn: 'cover_letter_count' 
    },
    'interview_prep': { 
      costColumn: 'interview_prep_cost_usd', 
      countColumn: 'interview_prep_count' 
    },
    'saved_job': { 
      costColumn: null, // No cost for saved jobs
      countColumn: 'saved_jobs_count' 
    },
    'google_drive': { 
      costColumn: 'other_cost_usd', 
      countColumn: null // No count tracking
    },
    'google_docs': { 
      costColumn: 'other_cost_usd', 
      countColumn: null // No count tracking
    },
  };
  
  // Internal operations only update cost
  const isTrackedOperation = data.operation in columnMapping;
  
  if (isTrackedOperation) {
    const mapping = columnMapping[data.operation as TrackedOperation];
    
    // Update cost if applicable
    if (mapping.costColumn && data.cost > 0) {
      await this.supabase.rpc('update_usage_cost', {
        p_user_id: data.userId,
        p_date: currentDate,
        p_cost: data.cost,
        p_column_name: mapping.costColumn,
      });
    }
    
    // Update count if applicable
    if (mapping.countColumn) {
      await this.supabase.rpc('update_usage_count', {
        p_user_id: data.userId,
        p_date: currentDate,
        p_column_name: mapping.countColumn,
      });
      
      // Also update daily stats
      await this.supabase.rpc('update_daily_count', {
        p_user_id: data.userId,
        p_date: currentDate,
        p_column_name: mapping.countColumn,
      });
    }
  } else {
    // Internal operations - cost only in other_cost_usd
    if (data.cost > 0) {
      await this.supabase.rpc('update_usage_cost', {
        p_user_id: data.userId,
        p_date: currentDate,
        p_cost: data.cost,
        p_column_name: 'other_cost_usd',
      });
    }
  }
  
  return { success: true, cost: data.cost };
}

6.3: Migration to Remove resume_analysis_count ​

sql
-- Migration: Remove unused resume_analysis columns

-- Remove from user_usage_summary
ALTER TABLE user_usage_summary 
DROP COLUMN IF EXISTS resume_analysis_count,
DROP COLUMN IF EXISTS resume_analysis_cost_usd;

-- Remove from user_daily_stats
ALTER TABLE user_daily_stats
DROP COLUMN IF EXISTS resume_analysis_count;

-- Drop old functions with business logic
DROP FUNCTION IF EXISTS update_user_usage(UUID, DATE, NUMERIC, TEXT);
DROP FUNCTION IF EXISTS update_daily_stats(UUID, DATE, TEXT);

Testing After Phase 6 ​

bash
# Run migrations
npx supabase db push

# Test the application
npm run build --workspace=apps/web
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web

Phase 7: Update TypeScript Types ​

Remove resume_analysis fields from types ​

typescript
// apps/web/src/types/usage.ts (or wherever these are defined)

// Before
export interface UsageSummary {
  job_analysis_count: number;
  job_analysis_cost_usd: number;
  resume_analysis_count: number;  // ← Remove
  resume_analysis_cost_usd: number;  // ← Remove
  resume_customization_count: number;
  // ...
}

// After
export interface UsageSummary {
  job_analysis_count: number;
  job_analysis_cost_usd: number;
  resume_customization_count: number;
  resume_customization_cost_usd: number;
  cover_letter_count: number;
  cover_letter_cost_usd: number;
  interview_prep_count: number;
  interview_prep_cost_usd: number;
  saved_jobs_count: number;
  other_cost_usd: number;
  total_cost_usd: number;
  // ...
}

Update API responses ​

typescript
// apps/web/src/app/api/usage/stats/route.ts

// Remove references to resume_analysis_count and resume_analysis_cost_usd
// Update the response structure accordingly

Testing After Phase 7 ​

bash
npm run build --workspace=apps/web
npm run lint --workspace=apps/web
npm run type-check --workspace=apps/web

Final Testing Checklist ​

Functional Tests ​

  • [ ] Job analysis counts as 1, not 2
  • [ ] Chrome extension shows correct daily counts
  • [ ] Dashboard shows correct usage percentages
  • [ ] Costs are properly tracked for all operations
  • [ ] Internal operations don't increment counts

Code Quality ​

  • [ ] All lint checks pass
  • [ ] All TypeScript checks pass
  • [ ] Build completes successfully
  • [ ] No console errors in browser

Database Verification ​

sql
-- Check that counts are correct
SELECT 
  user_id,
  job_analysis_count,
  resume_customization_count,
  cover_letter_count,
  interview_prep_count,
  saved_jobs_count,
  total_cost_usd
FROM user_usage_summary
WHERE user_id = '[test-user-id]'
ORDER BY created_at DESC
LIMIT 1;

-- Verify resume_analysis columns are gone
SELECT column_name 
FROM information_schema.columns 
WHERE table_name = 'user_usage_summary' 
  AND column_name LIKE '%resume_analysis%';
-- Should return 0 rows

Rollback Plan ​

If issues arise:

  1. Revert code changes: Git revert the commits
  2. Restore database functions: Keep backup of original functions
  3. Re-add columns if needed: Have migration ready to re-add resume_analysis columns

Success Metrics ​

  • Job analysis count matches actual number of analyses performed
  • No increase in error rates
  • Performance improvement from checking resume first
  • Cleaner codebase with ~500 fewer lines
  • Clear separation of concerns between LLM calls and tracking

Built with VitePress