nook marketing - utm, analytics, etc
This commit is contained in:
137
src/server/analytics-schema.test.ts
Normal file
137
src/server/analytics-schema.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it, beforeEach } from "bun:test";
|
||||
import { Database } from "bun:sqlite";
|
||||
import {
|
||||
VISITOR_ANALYTICS_ADDED_COLUMNS,
|
||||
VISITOR_ANALYTICS_COLUMNS,
|
||||
VISITOR_ANALYTICS_DDL,
|
||||
VISITOR_ANALYTICS_INSERT,
|
||||
VISITOR_ANALYTICS_INDEXES
|
||||
} from "./analytics-schema";
|
||||
|
||||
// Why an in-memory SQLite database: VisitorAnalytics lived only in the live
|
||||
// Turso database, so nothing in the repo proved the DDL and the INSERT
|
||||
// agreed. These tests apply the real DDL to a throwaway database and run the
|
||||
// real INSERT statement against it — the exact class of drift (a column in
|
||||
// the INSERT that the table does not have) fails here instead of silently in
|
||||
// production, where the buffer swallows write errors.
|
||||
|
||||
let db: Database;
|
||||
|
||||
const sampleArgs = [
|
||||
"row-1", // id
|
||||
null, // user_id
|
||||
"/nook/", // path
|
||||
"GET", // method
|
||||
"https://nook.freno.me/", // referrer
|
||||
"Mozilla/5.0 (Macintosh)", // user_agent
|
||||
"127.0.0.1", // ip_address
|
||||
"GB", // country
|
||||
"desktop", // device_type
|
||||
"safari", // browser
|
||||
"macos", // os
|
||||
"hn", // utm_source
|
||||
"post", // utm_medium
|
||||
"launch", // utm_campaign
|
||||
12, // duration_ms
|
||||
100, // fcp
|
||||
200, // lcp
|
||||
0.01, // cls
|
||||
1, // fid
|
||||
2, // inp
|
||||
3, // ttfb
|
||||
4, // dom_load
|
||||
5 // load_complete
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
db.run(VISITOR_ANALYTICS_DDL);
|
||||
for (const index of VISITOR_ANALYTICS_INDEXES) db.run(index);
|
||||
});
|
||||
|
||||
describe("VisitorAnalytics schema", () => {
|
||||
it("creates the table idempotently", () => {
|
||||
expect(() => db.run(VISITOR_ANALYTICS_DDL)).not.toThrow();
|
||||
expect(() => {
|
||||
for (const index of VISITOR_ANALYTICS_INDEXES) db.run(index);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("defines every column the writers insert", () => {
|
||||
const columns = (
|
||||
db.prepare(`PRAGMA table_info(VisitorAnalytics)`).all() as {
|
||||
name: string;
|
||||
}[]
|
||||
).map((c) => c.name);
|
||||
|
||||
for (const column of VISITOR_ANALYTICS_COLUMNS) {
|
||||
expect(columns).toContain(column);
|
||||
}
|
||||
// The reverse direction matters too: a column the code writes but the
|
||||
// DDL omits is the failure this whole file exists to catch.
|
||||
expect(VISITOR_ANALYTICS_COLUMNS.length).toBe(23);
|
||||
});
|
||||
|
||||
it("has one placeholder per inserted column", () => {
|
||||
const placeholders = (VISITOR_ANALYTICS_INSERT.match(/\?/g) ?? []).length;
|
||||
expect(placeholders).toBe(VISITOR_ANALYTICS_COLUMNS.length);
|
||||
expect(sampleArgs.length).toBe(VISITOR_ANALYTICS_COLUMNS.length);
|
||||
});
|
||||
|
||||
it("accepts a full row and returns the campaign columns", () => {
|
||||
db.prepare(VISITOR_ANALYTICS_INSERT).run(...sampleArgs);
|
||||
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT path, country, utm_source, utm_medium, utm_campaign
|
||||
FROM VisitorAnalytics`
|
||||
)
|
||||
.get() as Record<string, unknown>;
|
||||
|
||||
expect(row).toEqual({
|
||||
path: "/nook/",
|
||||
country: "GB",
|
||||
utm_source: "hn",
|
||||
utm_medium: "post",
|
||||
utm_campaign: "launch"
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a row with no campaign tags", () => {
|
||||
const args = [...sampleArgs];
|
||||
args[11] = args[12] = args[13] = null;
|
||||
db.prepare(VISITOR_ANALYTICS_INSERT).run(...args);
|
||||
|
||||
const row = db
|
||||
.prepare(`SELECT utm_source FROM VisitorAnalytics`)
|
||||
.get() as Record<string, unknown>;
|
||||
expect(row.utm_source).toBeNull();
|
||||
});
|
||||
|
||||
it("adds the post-hoc columns to a table that predates them", () => {
|
||||
// Simulate the older table: create it without the utm columns, then run
|
||||
// the ALTER path the bootstrap uses and confirm the writers still work.
|
||||
const old = new Database(":memory:");
|
||||
const withoutUtm = VISITOR_ANALYTICS_COLUMNS.filter(
|
||||
(c) => !VISITOR_ANALYTICS_ADDED_COLUMNS.some((a) => a.name === c)
|
||||
);
|
||||
old.run(
|
||||
`CREATE TABLE VisitorAnalytics (${withoutUtm
|
||||
.map((c) => `${c} TEXT`)
|
||||
.join(", ")})`
|
||||
);
|
||||
|
||||
const present = new Set(
|
||||
(old.prepare(`PRAGMA table_info(VisitorAnalytics)`).all() as {
|
||||
name: string;
|
||||
}[]).map((c) => c.name)
|
||||
);
|
||||
for (const column of VISITOR_ANALYTICS_ADDED_COLUMNS) {
|
||||
if (!present.has(column.name)) old.run(column.ddl);
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
old.prepare(VISITOR_ANALYTICS_INSERT).run(...sampleArgs)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user