Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/modules/creators/creators.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { attachTimestampHeader } from '../../utils/timestamp-headers.utils';
import { parsePublicQuery } from '../../utils/public-query-parse.utils';
import { buildOffsetPaginationMeta } from '../../utils/pagination.utils';
import { buildCreatorListRequestContext } from './creator-list-context.utils';
import {
incrementFilterParseError,
type FilterParseErrorCategory,
} from '../../utils/filter-parse-metrics.utils';

/**
* Controller for GET /api/v1/creators
Expand All @@ -28,6 +32,9 @@ export const httpListCreators: AsyncController = async (req, res, next) => {
// Validate query parameters
const parsed = parsePublicQuery(CreatorListQuerySchema, ctx.query);
if (!parsed.ok) {
// Increment filter parse error counter
const category = categorizeParseError(parsed.details);
incrementFilterParseError('/api/v1/creators', category);
return sendValidationError(res, 'Invalid query parameters', parsed.details);
}
const validatedQuery = parsed.data;
Expand All @@ -51,6 +58,23 @@ export const httpListCreators: AsyncController = async (req, res, next) => {
}
};

/**
* Categorize a parse error based on the validation details.
*
* @param details - Validation error details from parsePublicQuery
* @returns The error category for metrics labeling
*/
function categorizeParseError(
details: Array<{ field: string; message: string }>
): FilterParseErrorCategory {
// Check for unknown key errors (strict mode violations)
if (details.some(d => d.message.includes('unrecognized') || d.message.includes('unknown'))) {
return 'unknown_key';
}
// Default to invalid_value for type/range errors
return 'invalid_value';
}

/**
* Controller for GET /api/v1/creators/:id/stats
*
Expand Down
49 changes: 49 additions & 0 deletions src/utils/filter-parse-metrics.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// src/utils/filter-parse-metrics.utils.test.ts
// Unit tests for filter parse error metrics counter.

import { strict as assert } from 'assert';
import {
incrementFilterParseError,
getFilterParseErrors,
resetFilterParseMetrics,
} from './filter-parse-metrics.utils';

function run() {
// Reset before each test
resetFilterParseMetrics();

// --- initial state ---
assert.deepEqual(getFilterParseErrors(), [], 'should start empty');

// --- single increment ---
incrementFilterParseError('/api/v1/creators', 'unknown_key');
const errors = getFilterParseErrors();
assert.equal(errors.length, 1, 'should have one entry');
assert.equal(errors[0].route, '/api/v1/creators');
assert.equal(errors[0].category, 'unknown_key');
assert.equal(errors[0].count, 1);

// --- multiple increments same key ---
incrementFilterParseError('/api/v1/creators', 'unknown_key');
const errors2 = getFilterParseErrors();
assert.equal(errors2.length, 1, 'should still have one entry');
assert.equal(errors2[0].count, 2, 'count should be 2');

// --- different category ---
incrementFilterParseError('/api/v1/creators', 'invalid_value');
const errors3 = getFilterParseErrors();
assert.equal(errors3.length, 2, 'should have two entries');

// --- different route ---
incrementFilterParseError('/api/v1/other', 'unknown_key');
const errors4 = getFilterParseErrors();
assert.equal(errors4.length, 3, 'should have three entries');

// --- reset ---
resetFilterParseMetrics();
assert.deepEqual(getFilterParseErrors(), [], 'should be empty after reset');

console.log('filter-parse-metrics.utils tests passed');
}

run();
63 changes: 63 additions & 0 deletions src/utils/filter-parse-metrics.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Lightweight in-process counter for filter parse errors.
*
* Tracks validation failures on creator list endpoints so operators can
* monitor malformed or malicious query patterns without external dependencies.
*/

export type FilterParseErrorCategory = 'unknown_key' | 'invalid_value' | 'schema_error';

export interface FilterParseErrorEntry {
route: string;
category: FilterParseErrorCategory;
count: number;
lastOccurred: string;
}

const counters = new Map<string, FilterParseErrorEntry>();

function key(route: string, category: FilterParseErrorCategory): string {
return `${route}:${category}`;
}

/**
* Increment the error counter for a specific route and error category.
*
* @param route - The API route where the error occurred (e.g., '/api/v1/creators')
* @param category - The type of parse error
*/
export function incrementFilterParseError(
route: string,
category: FilterParseErrorCategory
): void {
const k = key(route, category);
const existing = counters.get(k);
if (existing) {
existing.count += 1;
existing.lastOccurred = new Date().toISOString();
} else {
counters.set(k, {
route,
category,
count: 1,
lastOccurred: new Date().toISOString(),
});
}
}

/**
* Get all filter parse error counters.
*
* @returns Array of error entries with route, category, count, and last occurrence
*/
export function getFilterParseErrors(): FilterParseErrorEntry[] {
return Array.from(counters.values());
}

/**
* Reset all filter parse error counters.
* Primarily for testing.
*/
export function resetFilterParseMetrics(): void {
counters.clear();
}
Loading