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
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ cd apps/api && bun test
cd apps/basket && bun test
cd packages/sdk && bun test
cd packages/tracker && bun run test:unit
cd packages/db && bun run db:seed
bun run workspace --reset
```

## Search Patterns
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ bun run test:watch
bun run db:push # Apply schema changes (no migration files)
bun run db:migrate # Run migration files
bun run db:studio # Open Drizzle Studio GUI
bun run db:seed <WEBSITE_ID> [EVENT_COUNT] # Seed sample analytics data
bun run workspace [--reset] [--anomaly] [--events N] [--website <ID>] # Local login, website and seeded analytics

# SDK (must build before dev if SDK changed)
bun run sdk:build
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ bun run test:watch
bun run db:push # Apply schema changes (no migration files)
bun run db:migrate # Run migration files
bun run db:studio # Open Drizzle Studio GUI
bun run db:seed <WEBSITE_ID> [EVENT_COUNT] # Seed sample analytics data
bun run workspace [--reset] [--anomaly] [--events N] [--website <ID>] # Local login, website and seeded analytics

# SDK (must build before dev if SDK changed)
bun run sdk:build
Expand Down
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ Open [localhost:3000](http://localhost:3000). The dev command builds the SDK and
devtools for you. Add provider keys only for the features you're working on;
see [optional services](README.md#optional-services).

To explore with sample data, create a website, copy its ID from its settings,
and run `bun run db:seed YOUR_WEBSITE_ID 1000`.
To explore with sample data, run `bun run workspace --reset`. It targets the
local Docker services only and prints the website. Without `--website`, it
prints a password for a newly created default account or directs you to use
the existing account's password. It prints an API key only when it creates
the default website. Add `--anomaly` for a traffic spike and error surge that
Insights will flag, or
`--website YOUR_WEBSITE_ID` to seed a website you already made. The optional
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`--events N` sets an approximate event target of up to 100,000 across 28 days;
session sizes, weekend traffic and anomaly variation affect the number written.
The command prints the actual inserted counts.

## Check your changes

Expand Down
171 changes: 33 additions & 138 deletions apps/dashboard/app/api/test/e2e/clickhouse/route.ts
Original file line number Diff line number Diff line change
@@ -1,162 +1,57 @@
import { clickHouse, TABLE_NAMES } from "@databuddy/db/clickhouse";
import { clickHouse } from "@databuddy/db/clickhouse";
import { generateAnalytics, seedAnalytics } from "@databuddy/db/seed";
import { readBooleanEnv } from "@databuddy/env/app";
import { z } from "zod";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const TEST_KEY_HEADER = "x-e2e-test-key";
const PATHS = ["/", "/pricing", "/docs", "/dashboard", "/settings"];
const REFERRERS = [null, "https://google.com", "https://github.com"];
const seedBody = z.object({
eventCount: z.coerce.number().catch(250),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
websiteId: z.string().min(1),
});

interface SeedBody {
eventCount?: unknown;
websiteId?: unknown;
}

function isE2EModeEnabled(): boolean {
return readBooleanEnv("DATABUDDY_E2E_MODE");
}

function notFound(): Response {
return Response.json({ error: "Not found" }, { status: 404 });
function countBy<T>(items: T[], key: (item: T) => string) {
const counts: Record<string, number> = {};
for (const item of items) {
counts[key(item)] = (counts[key(item)] ?? 0) + 1;
}
return counts;
}

function assertE2EAccess(request: Request): Response | null {
if (!isE2EModeEnabled()) {
return notFound();
}
export async function POST(request: Request): Promise<Response> {
const key = process.env.DATABUDDY_E2E_TEST_KEY;
if (!key) {
return notFound();
if (!(readBooleanEnv("DATABUDDY_E2E_MODE") && key)) {
return Response.json({ error: "Not found" }, { status: 404 });
}
if (request.headers.get(TEST_KEY_HEADER) !== key) {
if (request.headers.get("x-e2e-test-key") !== key) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return null;
}

function normalizeEventCount(value: unknown): number {
const parsed = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(parsed)) {
return 250;
}
return Math.min(Math.max(Math.floor(parsed), 1), 5000);
}

function clickHouseDate(date: Date): string {
return date.toISOString().replace("T", " ").replace("Z", "");
}

function pageTitle(path: string): string {
return path === "/" ? "Home" : path.slice(1).replaceAll("-", " ");
}

export async function POST(request: Request): Promise<Response> {
const denied = assertE2EAccess(request);
if (denied) {
return denied;
}

const body = (await request.json().catch(() => ({}))) as SeedBody;
if (typeof body.websiteId !== "string" || !body.websiteId) {
const body = seedBody.safeParse(await request.json().catch(() => null));
if (!body.success) {
return Response.json({ error: "websiteId is required" }, { status: 400 });
}
const { eventCount, websiteId } = body.data;

const eventCount = normalizeEventCount(body.eventCount);

const now = Date.now();
const users = Array.from(
{ length: Math.max(3, Math.ceil(eventCount / 40)) },
() => crypto.randomUUID()
);
const sessions = Array.from(
{ length: Math.max(5, Math.ceil(eventCount / 12)) },
(_, index) => ({
anonymousId: users[index % users.length],
sessionId: crypto.randomUUID(),
start: now - (eventCount - index) * 60_000,
})
);

const events = Array.from({ length: eventCount }, (_, index) => {
const session = sessions[index % sessions.length];
const path = PATHS[index % PATHS.length];
const timestamp = new Date(session.start + index * 30_000);
return {
id: crypto.randomUUID(),
client_id: body.websiteId as string,
event_name: index % 4 === 0 ? "page_exit" : "screen_view",
anonymous_id: session.anonymousId,
time: clickHouseDate(timestamp),
session_id: session.sessionId,
session_start_time: clickHouseDate(new Date(session.start)),
timestamp: clickHouseDate(timestamp),
referrer: REFERRERS[index % REFERRERS.length],
url: `https://e2e.databuddy.local${path}`,
path,
title: pageTitle(path),
ip: "127.0.0.1",
user_agent: "Databuddy E2E",
browser_name: index % 3 === 0 ? "Firefox" : "Chrome",
os_name: index % 2 === 0 ? "macOS" : "Linux",
device_type: index % 5 === 0 ? "mobile" : "desktop",
country: index % 2 === 0 ? "US" : "DE",
properties: "{}",
created_at: clickHouseDate(new Date(now)),
};
const rows = generateAnalytics({
clientId: websiteId,
days: 1,
domain: "e2e.databuddy.local",
events: Math.min(Math.max(Math.floor(eventCount), 1), 5000),
});
await seedAnalytics(clickHouse, rows);

const screenViewEvents = events.filter(
const screenViews = rows.events.filter(
(event) => event.event_name === "screen_view"
);
const screenViewsByCountry = screenViewEvents.reduce<Record<string, number>>(
(acc, event) => {
acc[event.country] = (acc[event.country] ?? 0) + 1;
return acc;
},
{}
);
const screenViewsByPath = screenViewEvents.reduce<Record<string, number>>(
(acc, event) => {
acc[event.path] = (acc[event.path] ?? 0) + 1;
return acc;
},
{}
);

const outgoingLinks = sessions
.slice(0, Math.ceil(eventCount / 20))
.map((session) => ({
id: crypto.randomUUID(),
client_id: body.websiteId as string,
anonymous_id: session.anonymousId,
session_id: session.sessionId,
href: "https://github.com/databuddy-analytics/Databuddy",
text: "Databuddy GitHub",
properties: "{}",
timestamp: clickHouseDate(new Date(session.start + 45_000)),
}));

await Promise.all([
clickHouse.insert({
format: "JSONEachRow",
table: TABLE_NAMES.events,
values: events,
}),
clickHouse.insert({
format: "JSONEachRow",
table: TABLE_NAMES.outgoing_links,
values: outgoingLinks,
}),
]);

return Response.json({
events: events.length,
outgoingLinks: outgoingLinks.length,
screenViews: screenViewEvents.length,
screenViewsByCountry,
screenViewsByPath,
events: rows.events.length,
outgoingLinks: rows.outgoingLinks.length,
screenViews: screenViews.length,
screenViewsByCountry: countBy(screenViews, (event) => event.country),
screenViewsByPath: countBy(screenViews, (event) => event.path),
seeded: true,
websiteId: body.websiteId,
websiteId,
});
}
2 changes: 1 addition & 1 deletion apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "dotenv -e ../../.env -- bun --watch run src/index.ts",
"test": "bun test src --path-ignore-patterns='**/*.integration.test.ts'",
"test:integration": "INSIGHTS_INTEGRATION_TESTS=true BULLMQ_REDIS_URL=\"${BULLMQ_REDIS_URL:-redis://localhost:6379/4}\" bun test --isolate src/scheduler.integration.test.ts src/idempotency.integration.test.ts src/selection-billing.integration.test.ts src/investigation-billing.integration.test.ts src/generation-billing.integration.test.ts src/resume-clarification.integration.test.ts src/business-context.integration.test.ts src/retention-retirement.integration.test.ts",
"test:integration": "INSIGHTS_INTEGRATION_TESTS=true BULLMQ_REDIS_URL=\"${BULLMQ_REDIS_URL:-redis://localhost:6379/4}\" bun test --isolate src/scheduler.integration.test.ts src/idempotency.integration.test.ts src/selection-billing.integration.test.ts src/investigation-billing.integration.test.ts src/generation-billing.integration.test.ts src/resume-clarification.integration.test.ts src/business-context.integration.test.ts src/retention-retirement.integration.test.ts src/detection.integration.test.ts",
"check-types": "tsc --noEmit"
},
"dependencies": {
Expand Down
42 changes: 42 additions & 0 deletions apps/insights/src/detection.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "@databuddy/test/env";
import { afterAll, describe, expect, it } from "bun:test";
import { clickHouse } from "@databuddy/db/clickhouse";
import {
deleteAnalytics,
generateAnalytics,
seedAnalytics,
} from "@databuddy/db/seed";
import { detectSignals } from "./detection";
import { TRAFFIC_METRICS } from "./investigation";

const describeIntegration =
process.env.INSIGHTS_INTEGRATION_TESTS === "true" ? describe : describe.skip;
Comment thread
izadoesdev marked this conversation as resolved.

const websiteId = `detection-fixture-${crypto.randomUUID()}`;

describeIntegration("detectSignals on the workspace fixture", () => {
afterAll(() => deleteAnalytics(clickHouse, websiteId), 30_000);

it("flags the seeded anomaly", async () => {
await seedAnalytics(
clickHouse,
generateAnalytics({
anomaly: true,
clientId: websiteId,
domain: "localhost",
})
);

const signals = await detectSignals({
lookbackDays: 7,
timezone: "UTC",
websiteId,
});

const rising = signals
.filter((signal) => signal.direction === "up")
.map((signal) => signal.metric);
expect(rising.some((metric) => TRAFFIC_METRICS.has(metric))).toBe(true);
expect(rising).toContain("error_count");
}, 30_000);
});
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"db:push": "dotenv -- sh -c 'NODE_ENV=${NODE_ENV:-development} turbo run db:push \"$@\"' --",
"db:migrate": "dotenv -- turbo run db:migrate",
"db:deploy": "dotenv -- turbo run db:deploy",
"db:seed": "dotenv -- sh -c 'cd packages/db && bun run db:seed \"$@\"' --",
"workspace": "NODE_ENV=development bun --no-env-file packages/test/src/setup.ts",
"email:dev": "dotenv -- sh -c 'cd packages/email && bun run dev'",
"email:segments": "dotenv -- sh -c 'cd packages/db && bun run email:segments \"$@\"' --",
"sdk:build": "turbo run build --filter @databuddy/sdk",
Expand Down
6 changes: 3 additions & 3 deletions packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"./schema": "./src/drizzle/schema/index.ts",
"./schema/relations": "./src/drizzle/schema/relations.ts",
"./clickhouse": "./src/clickhouse/index.ts",
"./clickhouse/apply": "./src/clickhouse/apply.ts",
"./clickhouse/dql": "./src/clickhouse/dql.ts",
"./clickhouse/schema": "./src/clickhouse/schema.ts",
"./clickhouse/tables": "./src/clickhouse/schema/tables.generated.ts",
"./e2e-db-lifecycle": "./src/e2e-db-lifecycle.ts",
"./seed": "./src/seed.ts",
"./test-env": "./src/test-env.ts"
},
"scripts": {
Expand All @@ -23,7 +25,6 @@
"ch:check": "bun run generate && git diff --exit-code src/clickhouse/schema/tables.generated.ts",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx src/seed.ts",
"email:segments": "bun src/scripts/export-user-email-segments.ts",
"clickhouse:init": "bun src/clickhouse/setup.ts",
"test": "bun test src",
Expand All @@ -41,7 +42,6 @@
"pg": "^8.16.3"
},
"devDependencies": {
"@types/pg": "^8.16.0",
"tsx": "^4.21.0"
"@types/pg": "^8.16.0"
}
}
Loading
Loading