Skip to content

Commit ec1ff3b

Browse files
vklimontovichclaude
andcommitted
refactor: reorder analytics columns for readability, fix neon update error handling
Put timestamp/type/host/path first, move event_id and metadata last. ClickHouse ORDER BY now includes timestamp for efficient time-range queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6394b1b commit ec1ff3b

File tree

3 files changed

+83
-40
lines changed

3 files changed

+83
-40
lines changed

packages/core/src/backends/lib/db.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import type { ClientContext, NextlyticsEvent, ServerEventContext } from "../../types";
22

33
export const tableColumns = [
4-
{ name: "event_id", pgType: "TEXT PRIMARY KEY", chType: "String" },
5-
{ name: "parent_event_id", pgType: "TEXT", chType: "Nullable(String)" },
6-
{ name: "timestamp", pgType: "TIMESTAMPTZ", chType: "DateTime64(3)" },
7-
{ name: "type", pgType: "TEXT", chType: "LowCardinality(String)" },
8-
{ name: "anonymous_user_id", pgType: "TEXT", chType: "Nullable(String)" },
4+
{ name: "timestamp", pgType: "TIMESTAMPTZ NOT NULL", chType: "DateTime64(3)" },
5+
{ name: "type", pgType: "TEXT NOT NULL", chType: "LowCardinality(String)" },
6+
{ name: "host", pgType: "TEXT", chType: "LowCardinality(String)" },
7+
{ name: "path", pgType: "TEXT", chType: "String" },
8+
{ name: "method", pgType: "TEXT", chType: "LowCardinality(String)" },
99
{ name: "user_id", pgType: "TEXT", chType: "Nullable(String)" },
10+
{ name: "anonymous_user_id", pgType: "TEXT", chType: "Nullable(String)" },
1011
{ name: "user_email", pgType: "TEXT", chType: "Nullable(String)" },
1112
{ name: "user_name", pgType: "TEXT", chType: "Nullable(String)" },
12-
{ name: "host", pgType: "TEXT", chType: "LowCardinality(String)" },
13-
{ name: "method", pgType: "TEXT", chType: "LowCardinality(String)" },
14-
{ name: "path", pgType: "TEXT", chType: "String" },
1513
{ name: "ip", pgType: "INET", chType: "Nullable(IPv6)" },
1614
{ name: "referer", pgType: "TEXT", chType: "Nullable(String)" },
1715
{ name: "user_agent", pgType: "TEXT", chType: "Nullable(String)" },
1816
{ name: "locale", pgType: "TEXT", chType: "LowCardinality(Nullable(String))" },
17+
{ name: "event_id", pgType: "TEXT PRIMARY KEY", chType: "String" },
18+
{ name: "parent_event_id", pgType: "TEXT", chType: "Nullable(String)" },
1919
{ name: "server_context", pgType: "JSONB", chType: "JSON" },
2020
{ name: "client_context", pgType: "JSONB", chType: "JSON" },
2121
{ name: "user_traits", pgType: "JSONB", chType: "JSON" },
@@ -70,21 +70,21 @@ function extractCommonFields(event: NextlyticsEvent) {
7070
: null;
7171

7272
return {
73-
event_id: event.eventId,
74-
parent_event_id: event.parentEventId ?? null,
7573
timestamp: event.collectedAt,
7674
type: event.type,
77-
anonymous_user_id: event.anonymousUserId ?? null,
75+
host,
76+
path,
77+
method,
7878
user_id: event.userContext?.userId ?? null,
79+
anonymous_user_id: event.anonymousUserId ?? null,
7980
user_email: event.userContext?.traits?.email ?? null,
8081
user_name: event.userContext?.traits?.name ?? null,
81-
host,
82-
method,
83-
path,
8482
ip: ip || null,
8583
referer: clientCtx.referer ?? null,
8684
user_agent: clientCtx.user_agent ?? null,
8785
locale: clientCtx.locale ?? null,
86+
event_id: event.eventId,
87+
parent_event_id: event.parentEventId ?? null,
8888
serverContextRest,
8989
clientContextRest: clientCtx.rest,
9090
userTraitsRest,
@@ -119,9 +119,9 @@ export function eventToJsonRow(event: NextlyticsEvent): Record<ColumnName, unkno
119119

120120
// Postgres
121121
export function generatePgCreateTableSQL(tableName: string): string {
122-
const pk = tableColumns[0];
122+
const pk = tableColumns.find((c) => c.pgType.includes("PRIMARY KEY"))!;
123123
const alters = tableColumns
124-
.slice(1)
124+
.filter((c) => c !== pk)
125125
.map((col) => `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${col.name} ${col.pgType};`)
126126
.join("\n");
127127

@@ -142,7 +142,7 @@ export function generateChCreateTableSQL(database: string, tableName: string): s
142142
.join(", ");
143143
const create =
144144
`CREATE TABLE IF NOT EXISTS ${fullTable} (${createCols}) ` +
145-
`ENGINE = ReplacingMergeTree() PARTITION BY toYYYYMM(timestamp) ORDER BY event_id;`;
145+
`ENGINE = ReplacingMergeTree() PARTITION BY toYYYYMM(timestamp) ORDER BY (timestamp, event_id);`;
146146

147147
const alters = tableColumns
148148
.filter((c) => c.name !== "event_id" && c.name !== "timestamp")
@@ -158,21 +158,21 @@ export function isChTableNotFoundError(text: string): boolean {
158158

159159
/** Row type returned from analytics table queries */
160160
export interface AnalyticsEventRow {
161-
event_id: string;
162-
parent_event_id: string | null;
163161
timestamp: string;
164162
type: string;
165-
anonymous_user_id: string | null;
163+
host: string;
164+
path: string;
165+
method: string;
166166
user_id: string | null;
167+
anonymous_user_id: string | null;
167168
user_email: string | null;
168169
user_name: string | null;
169-
host: string;
170-
method: string;
171-
path: string;
172170
ip: string | null;
173171
referer: string | null;
174172
user_agent: string | null;
175173
locale: string | null;
174+
event_id: string;
175+
parent_event_id: string | null;
176176
server_context: Record<string, unknown>;
177177
client_context: Record<string, unknown>;
178178
user_traits: Record<string, unknown>;

packages/core/src/backends/neon.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,17 @@ export function neonBackend(config: NeonBackendConfig): NextlyticsBackend {
7070

7171
if (sets.length > 0) {
7272
params.push(eventId);
73-
await sql.query(
74-
`UPDATE ${table} SET ${sets.join(", ")} WHERE event_id = $${paramIndex}`,
75-
params
76-
);
73+
try {
74+
await sql.query(
75+
`UPDATE ${table} SET ${sets.join(", ")} WHERE event_id = $${paramIndex}`,
76+
params
77+
);
78+
} catch (err) {
79+
if (isPgTableNotFoundError(err)) {
80+
printCreateTableStatement();
81+
}
82+
throw err;
83+
}
7784
}
7885
},
7986
};

packages/website/src/copy/integrations/neon/documentation.mdx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,70 @@ database. It's optimized for serverless environments with connection pooling.
2525

2626
## Table Schema
2727

28-
On first use, if the table doesn't exist, Nextlytics will print the required CREATE TABLE statement.
29-
Run it in your Neon SQL editor:
28+
On first use, if the table doesn't exist, Nextlytics will print the required
29+
CREATE TABLE statement. Run it in your Neon SQL editor:
3030

3131
```sql
3232
CREATE TABLE IF NOT EXISTS analytics (
33-
event_id TEXT PRIMARY KEY,
33+
timestamp TIMESTAMPTZ NOT NULL,
3434
type TEXT NOT NULL,
35-
collected_at TIMESTAMPTZ NOT NULL,
36-
anonymous_user_id TEXT,
37-
user_id TEXT,
38-
path TEXT,
3935
host TEXT,
36+
path TEXT,
4037
method TEXT,
41-
ip TEXT,
38+
user_id TEXT,
39+
anonymous_user_id TEXT,
40+
user_email TEXT,
41+
user_name TEXT,
42+
ip INET,
4243
referer TEXT,
4344
user_agent TEXT,
4445
locale TEXT,
45-
properties JSONB,
46+
event_id TEXT PRIMARY KEY,
47+
parent_event_id TEXT,
4648
server_context JSONB,
4749
client_context JSONB,
48-
user_context JSONB
50+
user_traits JSONB,
51+
properties JSONB
4952
);
5053

51-
CREATE INDEX idx_analytics_collected_at ON analytics(collected_at DESC);
54+
CREATE INDEX idx_analytics_timestamp ON analytics(timestamp DESC);
5255
CREATE INDEX idx_analytics_type ON analytics(type);
5356
CREATE INDEX idx_analytics_user_id ON analytics(user_id);
5457
```
5558

59+
If you're using Prisma, add this model to your `schema.prisma` instead:
60+
61+
```prisma
62+
model Analytics {
63+
timestamp DateTime @db.Timestamptz()
64+
type String
65+
host String
66+
path String
67+
method String
68+
user_id String?
69+
anonymous_user_id String?
70+
user_email String?
71+
user_name String?
72+
ip String? @db.Inet
73+
referer String?
74+
user_agent String?
75+
locale String?
76+
event_id String @id
77+
parent_event_id String?
78+
server_context Json? @db.JsonB
79+
client_context Json? @db.JsonB
80+
user_traits Json? @db.JsonB
81+
properties Json? @db.JsonB
82+
83+
@@index([timestamp(sort: Desc)])
84+
@@index([type])
85+
@@index([user_id])
86+
@@map("analytics")
87+
}
88+
```
89+
90+
Then run `npx prisma db push` or add a migration with `npx prisma migrate dev`.
91+
5692
## Event Updates
5793

5894
Neon backend supports event updates. When client context becomes available (after the initial
@@ -85,10 +121,10 @@ FROM analytics
85121
WHERE type = 'purchase';
86122

87123
-- User journey
88-
SELECT type, path, collected_at
124+
SELECT type, path, timestamp
89125
FROM analytics
90126
WHERE user_id = 'user_123'
91-
ORDER BY collected_at;
127+
ORDER BY timestamp;
92128
```
93129

94130
## Branching

0 commit comments

Comments
 (0)