Skip to content

Commit

Permalink
feat: add update_stats functionality to ingest API
Browse files Browse the repository at this point in the history
- Introduced a new `update_stats` function to aggregate and update site statistics in the database, including monthly, yearly, and all-time views and unique visitors.
- Updated the ingest API to support the new `update_stats` task, enhancing the task management structure.
- Added unit tests for the `update_stats` function to ensure correct behavior and error handling.
  • Loading branch information
spences10 committed Jan 4, 2025
1 parent 71528ae commit d5c9fc5
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 31 deletions.
74 changes: 43 additions & 31 deletions src/routes/api/ingest/+server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { env } from '$env/dynamic/private'
import { json } from '@sveltejs/kit'
import { index_now } from './index-now'
import { update_embeddings } from './update-embeddings'
import { update_popular_posts } from './update-popular-posts'
import { update_posts } from './update-posts'
import { update_related_posts_table } from './update-related-posts'
import { env } from '$env/dynamic/private';
import { json } from '@sveltejs/kit';
import { index_now } from './index-now';
import { update_embeddings } from './update-embeddings';
import { update_popular_posts } from './update-popular-posts';
import { update_posts } from './update-posts';
import { update_related_posts_table } from './update-related-posts';
import { update_stats } from './update-stats';

// curl -X POST https://yourdomain.com/api/ingest \
// -H "Content-Type: application/json" \
Expand All @@ -13,7 +14,7 @@ import { update_related_posts_table } from './update-related-posts'
// Define generic task function
type TaskFunction<TArgs = any, TResult = any> = (
...args: TArgs[]
) => Promise<TResult>
) => Promise<TResult>;

// Define the type for the keys in tasks object
type TaskKey =
Expand All @@ -22,19 +23,20 @@ type TaskKey =
| 'update_related_posts'
| 'index_now'
| 'update_embeddings'
| 'update_stats';

// Define the type for tasks object
interface TaskType {
[key: string]: {
function: TaskFunction
expects_fetch: boolean
}
function: TaskFunction;
expects_fetch: boolean;
};
}

// Define the type for the expected structure of request body
interface RequestBody {
token: string
task: TaskKey
token: string;
task: TaskKey;
}

// Define a mapping from task names to functions
Expand All @@ -59,32 +61,42 @@ const tasks: TaskType = {
function: update_related_posts_table,
expects_fetch: false,
},
}
update_stats: {
function: update_stats,
expects_fetch: false,
},
};

export const POST = async ({ request, fetch }) => {
try {
const body: RequestBody = await request.json()
const token = body.token
const task_key = body.task
const body: RequestBody = await request.json();
const token = body.token;
const task_key = body.task;

if (!token || token !== env.INGEST_TOKEN) {
return json({ message: 'Unauthorized' }, { status: 401 })
return json({ message: 'Unauthorized' }, { status: 401 });
}

const task = tasks[task_key]
const task = tasks[task_key];
if (task && typeof task.function === 'function') {
console.log(`Executing task: ${task_key}`)
console.log(`Executing task: ${task_key}`);

try {
// Call the task function with or without fetch based on its requirement
const result = task.expects_fetch
? await task.function(fetch)
: await task.function()
: await task.function();

console.log(`Task ${task_key} completed with result:`, result)
return json(result)
console.log(
`Task ${task_key} completed with result:`,
result,
);
return json(result);
} catch (task_error) {
console.error(`Error executing task ${task_key}:`, task_error)
console.error(
`Error executing task ${task_key}:`,
task_error,
);
return json(
{
message: `Error executing task ${task_key}`,
Expand All @@ -94,7 +106,7 @@ export const POST = async ({ request, fetch }) => {
: 'Unknown error',
},
{ status: 500 },
)
);
}
} else {
return json(
Expand All @@ -103,20 +115,20 @@ export const POST = async ({ request, fetch }) => {
'Specified task does not exist or is not a function',
},
{ status: 400 },
)
);
}
} catch (error) {
console.error('Error in POST /api/ingest:', error)
console.error('Error in POST /api/ingest:', error);
const error_message =
error instanceof Error ? error.message : 'Unknown error'
const error_stack = error instanceof Error ? error.stack : ''
error instanceof Error ? error.message : 'Unknown error';
const error_stack = error instanceof Error ? error.stack : '';
return json(
{
message: 'Error processing the request',
error: error_message,
stack: error_stack,
},
{ status: 500 },
)
);
}
}
};
48 changes: 48 additions & 0 deletions src/routes/api/ingest/update-stats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { turso_client } from '$lib/turso';
import { describe, expect, it, vi } from 'vitest';
import { update_stats } from './update-stats';

vi.mock('$lib/turso', () => ({
turso_client: vi.fn(() => ({
execute: vi.fn().mockResolvedValue({ success: true }),
close: vi.fn(),
})),
}));

describe('update_stats', () => {
it('should successfully update stats', async () => {
const result = await update_stats();
expect(result).toEqual({
success: true,
message: 'Stats updated successfully',
});
});

it('should handle database errors', async () => {
const mock_client = {
execute: vi.fn().mockRejectedValue(new Error('Database error')),
close: vi.fn(),
};
vi.mocked(turso_client).mockReturnValue(mock_client);

await expect(update_stats()).rejects.toThrow(
'Failed to update stats: Database error',
);
expect(mock_client.close).toHaveBeenCalled();
});

it('should always close the database connection', async () => {
const mock_client = {
execute: vi.fn().mockRejectedValue(new Error('Test error')),
close: vi.fn(),
};
vi.mocked(turso_client).mockReturnValue(mock_client);

try {
await update_stats();
} catch {
// Ignore error
}
expect(mock_client.close).toHaveBeenCalled();
});
});
53 changes: 53 additions & 0 deletions src/routes/api/ingest/update-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { turso_client } from '$lib/turso';

const update_monthly_stats = `
INSERT OR REPLACE INTO analytics_monthly (pathname, year_month, views, unique_visitors)
SELECT
pathname,
strftime('%Y-%m', timestamp) as year_month,
SUM(views) as views,
SUM(uniques) as unique_visitors
FROM analytics_pages
WHERE pathname LIKE '/posts/%'
GROUP BY pathname, year_month;
`;

const update_yearly_stats = `
INSERT OR REPLACE INTO analytics_yearly (pathname, year, views, unique_visitors)
SELECT
pathname,
strftime('%Y', timestamp) as year,
SUM(views) as views,
SUM(uniques) as unique_visitors
FROM analytics_pages
WHERE pathname LIKE '/posts/%'
GROUP BY pathname, year;
`;

const update_all_time_stats = `
INSERT OR REPLACE INTO analytics_all_time (pathname, views, unique_visitors)
SELECT
pathname,
SUM(views) as views,
SUM(uniques) as unique_visitors
FROM analytics_pages
WHERE pathname LIKE '/posts/%'
GROUP BY pathname;
`;

export const update_stats = async () => {
const client = turso_client();
try {
await client.execute(update_monthly_stats);
await client.execute(update_yearly_stats);
await client.execute(update_all_time_stats);
return { success: true, message: 'Stats updated successfully' };
} catch (error) {
console.error('Error updating stats:', error);
throw new Error(
`Failed to update stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
} finally {
client.close();
}
};

0 comments on commit d5c9fc5

Please sign in to comment.