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
8 changes: 4 additions & 4 deletions src/adapters/realtime.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { BaseAdapter } from './base';
import type { AnalyticsEngineDataPoint } from '../types/realtime';
import { BaseAdapter } from './base.js';
import type { AnalyticsEngineDataPoint } from '../types/index.js';
import {
realtimeEventSchema,
type RealtimeEvent,
type ServerContext,
} from '../schemas/index.js';
import { parseUserAgent } from '../utils/user-agent-parser';
import { detectBot } from '../utils/bot-detection';
import { parseUserAgent } from '../utils/user-agent-parser.js';
import { detectBot } from '../utils/bot-detection.js';
Comment on lines +1 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

.js import extensions violate project coding guidelines

All four changed import lines add .js extensions, directly contradicting the project rule: "Never import .js files — use .ts imports only (TypeScript will resolve to .js at runtime)."

🔧 Proposed fix
-import { BaseAdapter } from './base.js';
-import type { AnalyticsEngineDataPoint } from '../types/index.js';
+import { BaseAdapter } from './base';
+import type { AnalyticsEngineDataPoint } from '../types/index';
 import {
   realtimeEventSchema,
   type RealtimeEvent,
   type ServerContext,
 } from '../schemas/index.js';
-import { parseUserAgent } from '../utils/user-agent-parser.js';
-import { detectBot } from '../utils/bot-detection.js';
+import { parseUserAgent } from '../utils/user-agent-parser';
+import { detectBot } from '../utils/bot-detection';

Based on learnings: "Never import .js files - use .ts imports only (TypeScript will resolve to .js at runtime)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapters/realtime.ts` around lines 1 - 9, The imports for BaseAdapter,
AnalyticsEngineDataPoint, realtimeEventSchema, RealtimeEvent, ServerContext,
parseUserAgent, and detectBot currently include ".js" extensions which violates
the guideline; update each import to remove the ".js" suffix so they use the TS
module specifiers (e.g., import BaseAdapter from './base' and likewise import
the other symbols from their modules without ".js"), letting TypeScript resolve
to .js at runtime.


/**
* Adapter for real-time analytics events
Expand Down
6 changes: 3 additions & 3 deletions src/durable-objects/realtime-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type {
RealtimeEvent,
RealtimeStats,
AggregatedData,
} from '../types/realtime';
import { parseUserAgent } from '../utils/user-agent-parser';
import { detectBot } from '../utils/bot-detection';
} from '../types/realtime.js';
import { parseUserAgent } from '../utils/user-agent-parser.js';
import { detectBot } from '../utils/bot-detection.js';
Comment on lines +5 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Three new .js import extensions violate the project coding guideline.

🔧 Proposed fix
-} from '../types/realtime.js';
-import { parseUserAgent } from '../utils/user-agent-parser.js';
-import { detectBot } from '../utils/bot-detection.js';
+} from '../types/realtime';
+import { parseUserAgent } from '../utils/user-agent-parser';
+import { detectBot } from '../utils/bot-detection';

Based on learnings: "Applies to **/*.{ts,tsx} : Never import .js files - use .ts imports only (TypeScript will resolve to .js at runtime)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/durable-objects/realtime-aggregator.ts` around lines 5 - 7, The three
imports in realtime-aggregator.ts use explicit “.js” extensions which violate
the project guideline; update the import statements to remove the “.js” suffix
(e.g. import { parseUserAgent } from '../utils/user-agent-parser' and import {
detectBot } from '../utils/bot-detection' and the types import from
'../types/realtime') so TypeScript module resolution handles the runtime .js
mapping; keep the imported symbol names (parseUserAgent, detectBot) unchanged.


/**
* Durable Object for real-time analytics aggregation
Expand Down
21 changes: 16 additions & 5 deletions src/routes/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import {
createProject,
listProjects,
getProject,
countProjects,
} from '../services/project.js';

const MAX_LIMIT = 1000;

/**
* Create and configure projects API router
*/
Expand Down Expand Up @@ -68,15 +71,23 @@ export function createProjectsRouter(): Hono<{ Bindings: Env }> {
*/
app.get('/', async (c) => {
try {
const limit = parseInt(c.req.query('limit') || '100');
const offset = parseInt(c.req.query('offset') || '0');

const projects = await listProjects(c.env.DB, limit, offset);
const rawLimit = parseInt(c.req.query('limit') ?? '', 10);
const limit =
Number.isNaN(rawLimit) || rawLimit < 0
? 100
: Math.min(rawLimit, MAX_LIMIT);
const rawOffset = parseInt(c.req.query('offset') ?? '', 10);
const offset = Number.isNaN(rawOffset) || rawOffset < 0 ? 0 : rawOffset;

const [projects, total] = await Promise.all([
listProjects(c.env.DB, limit, offset),
countProjects(c.env.DB),
]);

const response: ProjectListResponse = {
success: true,
projects,
total: projects.length,
total,
};

return c.json(response);
Expand Down
6 changes: 3 additions & 3 deletions src/routes/realtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import type { Env } from '../types/index';
import { RealtimeAdapter } from '../adapters/realtime';
import type { ServerContext, RealtimeEvent } from '../types/realtime';
import type { Env } from '../types/index.js';
import { RealtimeAdapter } from '../adapters/realtime.js';
import type { ServerContext, RealtimeEvent } from '../types/realtime.js';
Comment on lines +2 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same .js import extension violation as in src/adapters/realtime.ts

Lines 2–4 add .js extensions to local imports, violating the same guideline.

🔧 Proposed fix
-import type { Env } from '../types/index.js';
-import { RealtimeAdapter } from '../adapters/realtime.js';
-import type { ServerContext, RealtimeEvent } from '../types/realtime.js';
+import type { Env } from '../types/index';
+import { RealtimeAdapter } from '../adapters/realtime';
+import type { ServerContext, RealtimeEvent } from '../types/realtime';

Based on learnings: "Never import .js files - use .ts imports only (TypeScript will resolve to .js at runtime)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { Env } from '../types/index.js';
import { RealtimeAdapter } from '../adapters/realtime.js';
import type { ServerContext, RealtimeEvent } from '../types/realtime.js';
import type { Env } from '../types/index';
import { RealtimeAdapter } from '../adapters/realtime';
import type { ServerContext, RealtimeEvent } from '../types/realtime';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/realtime.ts` around lines 2 - 4, The imports in this module use
explicit .js extensions which violates the project guideline; update the import
specifiers to remove the .js suffix so TypeScript resolves to .js at runtime —
e.g., change imports that reference '../types/index.js',
'../adapters/realtime.js', and '../types/realtime.js' to '../types/index',
'../adapters/realtime', and '../types/realtime' respectively, keeping the same
imported symbols (Env, RealtimeAdapter, ServerContext, RealtimeEvent).


const realtimeRouter = new Hono<{ Bindings: Env }>();
const adapter = new RealtimeAdapter();
Expand Down
22 changes: 11 additions & 11 deletions src/services/analytics-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export class AnalyticsEngineService {
* Write data point to Analytics Engine dataset with retry logic
* @returns Success status
*/
writeDataPoint<T>(
async writeDataPoint<T>(
env: Env,
datasetName: keyof Env,
adapter: DataAdapter<T>,
rawData: unknown
): { success: boolean; error?: string } {
): Promise<{ success: boolean; error?: string }> {
try {
// Validate input data
if (!adapter.validate(rawData)) {
Expand All @@ -42,7 +42,11 @@ export class AnalyticsEngineService {
}

// Write data point with retry logic
const writeResult = this.writeWithRetry(dataset, dataPoint, datasetName);
const writeResult = await this.writeWithRetry(
dataset,
dataPoint,
datasetName
);
if (!writeResult.success) {
return writeResult;
}
Expand Down Expand Up @@ -75,11 +79,11 @@ export class AnalyticsEngineService {
/**
* Write data point with exponential backoff retry logic
*/
private writeWithRetry(
private async writeWithRetry(
dataset: AnalyticsEngineDataset,
dataPoint: AnalyticsEngineDataPoint,
datasetName: keyof Env
): { success: boolean; error?: string } {
): Promise<{ success: boolean; error?: string }> {
let lastError: Error | undefined;

for (
Expand Down Expand Up @@ -115,12 +119,8 @@ export class AnalyticsEngineService {
}
);

// Exponential backoff delay (synchronous for simplicity in edge runtime)
// Note: In real-world edge runtime, this would be async, but for testing we keep it sync
const start = Date.now();
while (Date.now() - start < delay) {
// Busy wait for delay
}
// Async exponential backoff — avoids blocking the V8 event loop
await new Promise<void>((r) => setTimeout(r, delay));
}
}
}
Expand Down
24 changes: 13 additions & 11 deletions src/services/analytics-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,19 @@ export class AnalyticsQueryService {
allValues.length
);

timeseries.forEach((point) => {
const zScore = Math.abs((point.value - mean) / stdDev);
if (zScore > 2.5 && allValues.length > 10) {
anomalies.push({
timestamp: point.timestamp,
description: `Unusual activity detected: ${Math.round(point.value)} events`,
severity: zScore > 3 ? 'high' : 'medium',
value: point.value,
});
}
});
if (stdDev > 0) {
timeseries.forEach((point) => {
const zScore = Math.abs((point.value - mean) / stdDev);
if (zScore > 2.5 && allValues.length > 10) {
anomalies.push({
timestamp: point.timestamp,
description: `Unusual activity detected: ${Math.round(point.value)} events`,
severity: zScore > 3 ? 'high' : 'medium',
value: point.value,
});
}
});
}

// Generate recommendations
if (trends.length > 0 && trends[0]?.direction === 'up') {
Expand Down
12 changes: 12 additions & 0 deletions src/services/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ export async function listProjects(
return results || [];
}

/**
* Count total number of projects
* @param db D1 database binding
* @returns Total project count
*/
export async function countProjects(db: D1Database): Promise<number> {
const result = await db
.prepare('SELECT COUNT(*) as count FROM projects')
.first<{ count: number }>();
return result?.count ?? 0;
}

/**
* Update last_used timestamp for a project
* @param db D1 database binding
Expand Down
4 changes: 2 additions & 2 deletions src/services/self-tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export class SelfTrackingService {
}

// Create async write operation
const writePromise = Promise.resolve().then(() => {
const result = this.analyticsService.writeDataPoint(
const writePromise = Promise.resolve().then(async () => {
const result = await this.analyticsService.writeDataPoint(
env,
'SELF_TRACKING_ANALYTICS',
this.adapter,
Expand Down
10 changes: 0 additions & 10 deletions src/types/realtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,3 @@ export interface AggregatedData {
visitor_id?: string;
}>;
}

/**
* Analytics Engine data point format
* Reference: https://developers.cloudflare.com/analytics/analytics-engine/
*/
export interface AnalyticsEngineDataPoint {
indexes?: string[]; // Max 1 index, max 96 bytes each
doubles?: number[]; // Numeric values
blobs?: string[]; // String values, max 5120 bytes each
}
2 changes: 1 addition & 1 deletion src/utils/fingerprint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FingerprintComponents, Fingerprint } from '../types/realtime';
import type { FingerprintComponents, Fingerprint } from '../types/realtime.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

.js import extension violates the project coding guideline.

The guideline requires: "Never import .js files - use .ts imports only (TypeScript will resolve to .js at runtime)." This change introduces a .js extension on a TypeScript-to-TypeScript import.

🔧 Proposed fix
-import type { FingerprintComponents, Fingerprint } from '../types/realtime.js';
+import type { FingerprintComponents, Fingerprint } from '../types/realtime';

Based on learnings: "Applies to **/*.{ts,tsx} : Never import .js files - use .ts imports only (TypeScript will resolve to .js at runtime)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { FingerprintComponents, Fingerprint } from '../types/realtime.js';
import type { FingerprintComponents, Fingerprint } from '../types/realtime';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/fingerprint.ts` at line 1, The import in src/utils/fingerprint.ts
uses a .js extension which violates the guideline; update the import of the
types FingerprintComponents and Fingerprint from '../types/realtime.js' to the
TypeScript import without the .js extension (e.g., '../types/realtime') so
TypeScript resolves to .js at runtime and the rule "Never import .js files" is
respected.


/**
* Generate a privacy-respecting fingerprint from browser components
Expand Down
8 changes: 4 additions & 4 deletions src/utils/route-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ export function createAnalyticsHandler<T>(
adapter: DataAdapter<T>,
analyticsService: AnalyticsEngineService
): {
handleGet: (c: Context<{ Bindings: Env }>) => Response;
handleGet: (c: Context<{ Bindings: Env }>) => Promise<Response>;
handlePost: (c: Context<{ Bindings: Env }>) => Promise<Response>;
} {
return {
/**
* Handle GET requests with query parameters
*/
handleGet: (c: Context<{ Bindings: Env }>): Response => {
handleGet: async (c: Context<{ Bindings: Env }>): Promise<Response> => {
const rawData = c.req.query() as Record<string, string | string[]>;
const projectId = c.get('project_id');

Expand All @@ -33,7 +33,7 @@ export function createAnalyticsHandler<T>(
const dataWithProject = projectId
? { ...rawData, project_id: projectId }
: rawData;
const result = analyticsService.writeDataPoint(
const result = await analyticsService.writeDataPoint(
c.env,
dataset,
adapter,
Expand Down Expand Up @@ -95,7 +95,7 @@ export function createAnalyticsHandler<T>(
projectId && !Array.isArray(rawData)
? { ...rawData, project_id: projectId }
: rawData;
const result = analyticsService.writeDataPoint(
const result = await analyticsService.writeDataPoint(
c.env,
dataset,
adapter,
Expand Down
80 changes: 80 additions & 0 deletions test/e2e/projects-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ describe('Projects API E2E', () => {
];

statement.all.mockResolvedValueOnce({ results: mockProjects });
statement.first.mockResolvedValueOnce({ count: 3 });

const app = createRouter();
const request = new Request('http://localhost/api/project');
Expand All @@ -260,6 +261,7 @@ describe('Projects API E2E', () => {

it('should handle empty project list', async () => {
statement.all.mockResolvedValueOnce({ results: [] });
statement.first.mockResolvedValueOnce({ count: 0 });

const app = createRouter();
const request = new Request('http://localhost/api/project');
Expand All @@ -277,6 +279,37 @@ describe('Projects API E2E', () => {
}
});

it('should return DB total, not page size (pagination test)', async () => {
// Page returns 1 project but DB has 10 total
statement.all.mockResolvedValueOnce({
results: [
{
id: 'proj2',
description: 'Project 2',
created_at: 2,
last_used: null,
},
],
});
statement.first.mockResolvedValueOnce({ count: 10 });

const app = createRouter();
const request = new Request(
'http://localhost/api/project?limit=1&offset=1'
);

const response = await app.fetch(request, env);
const data = (await response.json()) as
| ProjectListResponse
| ErrorResponse;

expect(response.status).toBe(200);
if ('success' in data && data.success) {
expect(data.projects).toHaveLength(1);
expect(data.total).toBe(10); // DB total, not page size
}
});

it('should support pagination with limit and offset', async () => {
statement.all.mockResolvedValueOnce({
results: [
Expand All @@ -288,6 +321,7 @@ describe('Projects API E2E', () => {
},
],
});
statement.first.mockResolvedValueOnce({ count: 1 });

const app = createRouter();
const request = new Request(
Expand All @@ -306,6 +340,52 @@ describe('Projects API E2E', () => {
expect(statement.bind).toHaveBeenCalledWith(1, 1);
});

it('should use default limit=100 and offset=0 for NaN params', async () => {
statement.all.mockResolvedValueOnce({ results: [] });
statement.first.mockResolvedValueOnce({ count: 0 });

const app = createRouter();
const request = new Request(
'http://localhost/api/project?limit=abc&offset=xyz'
);

const response = await app.fetch(request, env);

expect(response.status).toBe(200);
// NaN should fall back to defaults: limit=100, offset=0
expect(statement.bind).toHaveBeenCalledWith(100, 0);
});

it('should clamp negative limit to default and negative offset to 0', async () => {
statement.all.mockResolvedValueOnce({ results: [] });
statement.first.mockResolvedValueOnce({ count: 0 });

const app = createRouter();
const request = new Request(
'http://localhost/api/project?limit=-10&offset=-5'
);

const response = await app.fetch(request, env);

expect(response.status).toBe(200);
// Negative limit should default to 100, negative offset should be 0
expect(statement.bind).toHaveBeenCalledWith(100, 0);
});

it('should enforce MAX_LIMIT of 1000', async () => {
statement.all.mockResolvedValueOnce({ results: [] });
statement.first.mockResolvedValueOnce({ count: 0 });

const app = createRouter();
const request = new Request('http://localhost/api/project?limit=9999');

const response = await app.fetch(request, env);

expect(response.status).toBe(200);
// Limit above MAX_LIMIT should be clamped to 1000
expect(statement.bind).toHaveBeenCalledWith(1000, 0);
});

it('should return 500 on database error', async () => {
statement.all.mockRejectedValueOnce(
new Error('Database connection failed')
Expand Down
Loading