@sixb/pg 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/threads.d.ts +4 -0
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/magic-links.d.ts.map +1 -1
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +0 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -11
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -2
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +984 -80
- package/dist/index.js.map +20 -18
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-artifact-storage.d.ts +19 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/threads.ts +23 -0
- package/src/auth-storage/auth-storage.ts +1 -22
- package/src/auth-storage/magic-links.ts +0 -4
- package/src/auth-storage/oidc-attempts.ts +0 -4
- package/src/auth-storage/rows.ts +0 -6
- package/src/auth-storage/sessions.ts +18 -30
- package/src/auth-storage/shared.ts +3 -35
- package/src/index.ts +10 -0
- package/src/migrations/062-drop-auth-audience.sql +19 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations.ts +20 -0
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-artifact-storage.ts +325 -0
- package/src/pg-execution-storage.ts +52 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTIVITY_MAX_VISIT_MS,
|
|
3
|
+
type ActivitySourceState,
|
|
4
|
+
activityActor,
|
|
5
|
+
activityCalendarDays,
|
|
6
|
+
parseActivityAttribution,
|
|
7
|
+
planActivityAppend,
|
|
8
|
+
type StoredActivityEvent,
|
|
9
|
+
serializeActivityAttribution,
|
|
10
|
+
validateActivityAppend,
|
|
11
|
+
} from "@sixb/core/internal/storage"
|
|
12
|
+
import type {
|
|
13
|
+
ActivityCalendarInput,
|
|
14
|
+
ActivityCursor,
|
|
15
|
+
ActivityDay,
|
|
16
|
+
ActivityInteraction,
|
|
17
|
+
ActivityPageView,
|
|
18
|
+
ActivityPeriodInput,
|
|
19
|
+
ActivitySessionVisits,
|
|
20
|
+
ActivityStorage,
|
|
21
|
+
ActivitySummary,
|
|
22
|
+
ActivityUsersPage,
|
|
23
|
+
ActivityVisit,
|
|
24
|
+
ActivityVisitsPage,
|
|
25
|
+
AppendActivityInput,
|
|
26
|
+
AppendActivityResult,
|
|
27
|
+
ListActivitySessionVisitsInput,
|
|
28
|
+
ListActivityUsersInput,
|
|
29
|
+
ListActivityVisitsInput,
|
|
30
|
+
ReadActivityVisitInput,
|
|
31
|
+
StoredActivityVisitPage,
|
|
32
|
+
} from "@sixb/core/storage"
|
|
33
|
+
import type { SQLClient } from "./pg-client"
|
|
34
|
+
import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "./transactions"
|
|
35
|
+
|
|
36
|
+
// Sorts after every stored timestamp, so a first page starts from the newest row.
|
|
37
|
+
const NO_CURSOR = { at: new Date("9999-12-31T23:59:59.999Z"), id: "" }
|
|
38
|
+
|
|
39
|
+
/** PostgreSQL activity. An advisory lock per session serializes its batches across replicas. */
|
|
40
|
+
export class PgActivityStorage implements ActivityStorage {
|
|
41
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
42
|
+
|
|
43
|
+
async append(input: AppendActivityInput): Promise<AppendActivityResult> {
|
|
44
|
+
validateActivityAppend(input)
|
|
45
|
+
|
|
46
|
+
return runPgTransaction(this.sql, async (sql) => {
|
|
47
|
+
await lockAdvisoryKeys(sql, [
|
|
48
|
+
JSON.stringify(["activity", input.projectId, input.actor.type, input.sessionId]),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const plan = planActivityAppend(
|
|
52
|
+
input,
|
|
53
|
+
await readSource(sql, input),
|
|
54
|
+
await readStoredEvents(sql, input)
|
|
55
|
+
)
|
|
56
|
+
if (plan.status !== "append") {
|
|
57
|
+
return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { state } = plan
|
|
61
|
+
const lastSeenAt = new Date(state.lastSeenAt)
|
|
62
|
+
|
|
63
|
+
if (plan.newVisit) {
|
|
64
|
+
const attribution =
|
|
65
|
+
plan.newVisit.attribution === null
|
|
66
|
+
? null
|
|
67
|
+
: serializeActivityAttribution(plan.newVisit.attribution)
|
|
68
|
+
|
|
69
|
+
await sql`
|
|
70
|
+
INSERT INTO activity_visits (
|
|
71
|
+
project_id, id, actor_type, user_id, grant_id, session_id,
|
|
72
|
+
started_at, last_seen_at, page_views, interactions, attribution
|
|
73
|
+
) VALUES (
|
|
74
|
+
${input.projectId}, ${state.visitId}, ${input.actor.type},
|
|
75
|
+
${input.actor.type === "user" ? input.actor.userId : null},
|
|
76
|
+
${input.actor.type === "share" ? input.actor.grantId : null},
|
|
77
|
+
${input.sessionId}, ${lastSeenAt}, ${lastSeenAt}, ${plan.pageViews.length},
|
|
78
|
+
${plan.interactions.length}, ${attribution}::text::jsonb
|
|
79
|
+
)
|
|
80
|
+
`
|
|
81
|
+
} else {
|
|
82
|
+
await sql`
|
|
83
|
+
UPDATE activity_visits
|
|
84
|
+
SET
|
|
85
|
+
last_seen_at = ${lastSeenAt},
|
|
86
|
+
page_views = page_views + ${plan.pageViews.length},
|
|
87
|
+
interactions = interactions + ${plan.interactions.length}
|
|
88
|
+
WHERE project_id = ${input.projectId} AND id = ${state.visitId}
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const pageView of plan.pageViews) {
|
|
93
|
+
await sql`
|
|
94
|
+
INSERT INTO activity_page_views (
|
|
95
|
+
project_id, visit_id, sequence, id, route, received_at, attribution
|
|
96
|
+
) VALUES (
|
|
97
|
+
${input.projectId}, ${pageView.visitId}, ${pageView.sequence}, ${pageView.id},
|
|
98
|
+
${pageView.route}, ${pageView.receivedAt},
|
|
99
|
+
${serializeActivityAttribution(pageView.attribution)}::text::jsonb
|
|
100
|
+
)
|
|
101
|
+
`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const interaction of plan.interactions) {
|
|
105
|
+
await sql`
|
|
106
|
+
INSERT INTO activity_interactions (
|
|
107
|
+
project_id, visit_id, sequence, id, name, route, received_at
|
|
108
|
+
) VALUES (
|
|
109
|
+
${input.projectId}, ${interaction.visitId}, ${interaction.sequence},
|
|
110
|
+
${interaction.id}, ${interaction.name}, ${interaction.route}, ${interaction.receivedAt}
|
|
111
|
+
)
|
|
112
|
+
`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await sql`
|
|
116
|
+
INSERT INTO activity_sources (
|
|
117
|
+
project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
|
|
118
|
+
window_start, window_count
|
|
119
|
+
) VALUES (
|
|
120
|
+
${input.projectId}, ${input.actor.type}, ${input.sessionId}, ${state.visitId},
|
|
121
|
+
${lastSeenAt}, ${state.sequence}, ${new Date(state.windowStart)}, ${state.windowCount}
|
|
122
|
+
)
|
|
123
|
+
ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
|
|
124
|
+
visit_id = excluded.visit_id,
|
|
125
|
+
last_seen_at = excluded.last_seen_at,
|
|
126
|
+
sequence = excluded.sequence,
|
|
127
|
+
window_start = excluded.window_start,
|
|
128
|
+
window_count = excluded.window_count
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
return { status: "accepted", accepted: plan.accepted }
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async list(input: ListActivityVisitsInput): Promise<ActivityVisitsPage> {
|
|
136
|
+
const before = input.before ?? NO_CURSOR
|
|
137
|
+
const rows = await this.sql<VisitRow[]>`
|
|
138
|
+
SELECT ${this.visitColumns()}
|
|
139
|
+
FROM activity_visits v
|
|
140
|
+
WHERE ${this.overlapping(input)}
|
|
141
|
+
AND (
|
|
142
|
+
v.last_seen_at < ${before.at}
|
|
143
|
+
OR (v.last_seen_at = ${before.at} AND v.id < ${before.id})
|
|
144
|
+
)
|
|
145
|
+
ORDER BY v.last_seen_at DESC, v.id DESC
|
|
146
|
+
LIMIT ${input.limit + 1}
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
visits: rows.slice(0, input.limit).map(visitFromRow),
|
|
151
|
+
hasMore: rows.length > input.limit,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async users(input: ListActivityUsersInput): Promise<ActivityUsersPage> {
|
|
156
|
+
const before: ActivityCursor = input.before ?? NO_CURSOR
|
|
157
|
+
const rows = await this.sql<UserRow[]>`
|
|
158
|
+
SELECT
|
|
159
|
+
v.user_id,
|
|
160
|
+
MAX(v.last_seen_at) AS last_seen_at,
|
|
161
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
162
|
+
COUNT(p.id) AS page_views
|
|
163
|
+
FROM activity_visits v
|
|
164
|
+
LEFT JOIN activity_page_views p
|
|
165
|
+
ON p.project_id = v.project_id
|
|
166
|
+
AND p.visit_id = v.id
|
|
167
|
+
AND p.received_at >= ${input.from}
|
|
168
|
+
AND p.received_at < ${input.to}
|
|
169
|
+
WHERE ${this.overlapping(input)} AND v.actor_type = 'user'
|
|
170
|
+
GROUP BY v.user_id
|
|
171
|
+
HAVING MAX(v.last_seen_at) < ${before.at}
|
|
172
|
+
OR (MAX(v.last_seen_at) = ${before.at} AND v.user_id < ${before.id})
|
|
173
|
+
ORDER BY last_seen_at DESC, v.user_id DESC
|
|
174
|
+
LIMIT ${input.limit + 1}
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
users: rows.slice(0, input.limit).map((row) => ({
|
|
179
|
+
userId: row.user_id,
|
|
180
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
181
|
+
visits: Number(row.visits),
|
|
182
|
+
pageViews: Number(row.page_views),
|
|
183
|
+
})),
|
|
184
|
+
hasMore: rows.length > input.limit,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async summarize(input: ActivityPeriodInput): Promise<ActivitySummary> {
|
|
189
|
+
const [row] = await this.sql<SummaryRow[]>`
|
|
190
|
+
SELECT
|
|
191
|
+
COUNT(DISTINCT v.user_id) AS users,
|
|
192
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
193
|
+
COUNT(DISTINCT v.id) FILTER (WHERE v.actor_type = 'share') AS shared_visits,
|
|
194
|
+
COUNT(p.id) AS page_views
|
|
195
|
+
FROM activity_visits v
|
|
196
|
+
LEFT JOIN activity_page_views p
|
|
197
|
+
ON p.project_id = v.project_id
|
|
198
|
+
AND p.visit_id = v.id
|
|
199
|
+
AND p.received_at >= ${input.from}
|
|
200
|
+
AND p.received_at < ${input.to}
|
|
201
|
+
WHERE ${this.overlapping(input)}
|
|
202
|
+
`
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
users: Number(row?.users ?? 0),
|
|
206
|
+
visits: Number(row?.visits ?? 0),
|
|
207
|
+
sharedVisits: Number(row?.shared_visits ?? 0),
|
|
208
|
+
pageViews: Number(row?.page_views ?? 0),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async calendar(input: ActivityCalendarInput): Promise<readonly ActivityDay[]> {
|
|
213
|
+
const visits = await this.sql<{ started_at: Date | string; last_seen_at: Date | string }[]>`
|
|
214
|
+
SELECT v.started_at, v.last_seen_at
|
|
215
|
+
FROM activity_visits v
|
|
216
|
+
WHERE ${this.overlapping(input)}
|
|
217
|
+
`
|
|
218
|
+
const days = await this.sql<{ day: string; page_views: string }[]>`
|
|
219
|
+
SELECT
|
|
220
|
+
to_char(p.received_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
|
|
221
|
+
COUNT(*) AS page_views
|
|
222
|
+
FROM activity_visits v
|
|
223
|
+
JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
|
|
224
|
+
WHERE ${this.overlapping(input)}
|
|
225
|
+
AND p.received_at >= ${input.from}
|
|
226
|
+
AND p.received_at < ${input.to}
|
|
227
|
+
GROUP BY day
|
|
228
|
+
`
|
|
229
|
+
|
|
230
|
+
return activityCalendarDays(
|
|
231
|
+
input,
|
|
232
|
+
visits.map((visit) => ({
|
|
233
|
+
startedAt: new Date(visit.started_at),
|
|
234
|
+
lastSeenAt: new Date(visit.last_seen_at),
|
|
235
|
+
})),
|
|
236
|
+
new Map(days.map((row) => [row.day, Number(row.page_views)]))
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async read(input: ReadActivityVisitInput): Promise<StoredActivityVisitPage | null> {
|
|
241
|
+
const [visit] = await this.sql<(VisitRow & { readonly session_id: string })[]>`
|
|
242
|
+
SELECT ${this.visitColumns()}, v.session_id
|
|
243
|
+
FROM activity_visits v
|
|
244
|
+
WHERE v.project_id = ${input.projectId} AND v.id = ${input.id}
|
|
245
|
+
`
|
|
246
|
+
if (!visit) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Page views and interactions share the visit's sequence, so one bound covers both.
|
|
251
|
+
const rows = await this.sql<EventRow[]>`
|
|
252
|
+
SELECT id, visit_id, sequence, route, NULL::text AS name, received_at
|
|
253
|
+
FROM activity_page_views
|
|
254
|
+
WHERE project_id = ${input.projectId}
|
|
255
|
+
AND visit_id = ${input.id}
|
|
256
|
+
UNION ALL
|
|
257
|
+
SELECT id, visit_id, sequence, route, name, received_at
|
|
258
|
+
FROM activity_interactions
|
|
259
|
+
WHERE project_id = ${input.projectId}
|
|
260
|
+
AND visit_id = ${input.id}
|
|
261
|
+
ORDER BY sequence
|
|
262
|
+
LIMIT ${input.limit + 1}
|
|
263
|
+
`
|
|
264
|
+
|
|
265
|
+
const pageViews: ActivityPageView[] = []
|
|
266
|
+
const interactions: ActivityInteraction[] = []
|
|
267
|
+
for (const row of rows.slice(0, input.limit)) {
|
|
268
|
+
if (row.name === null) {
|
|
269
|
+
pageViews.push(pageViewFromRow(row))
|
|
270
|
+
} else {
|
|
271
|
+
interactions.push({ ...pageViewFromRow(row), name: row.name })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
visit: visitFromRow(visit),
|
|
277
|
+
sessionId: visit.session_id,
|
|
278
|
+
pageViews,
|
|
279
|
+
interactions,
|
|
280
|
+
hasMore: rows.length > input.limit,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async sessionVisits(
|
|
285
|
+
input: ListActivitySessionVisitsInput
|
|
286
|
+
): Promise<readonly ActivitySessionVisits[]> {
|
|
287
|
+
if (input.sessionIds.length === 0) {
|
|
288
|
+
return []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A session opens a visit per return after inactivity, so each has a few to rank.
|
|
292
|
+
const rows = await this.sql<SessionVisitRow[]>`
|
|
293
|
+
SELECT *
|
|
294
|
+
FROM (
|
|
295
|
+
SELECT
|
|
296
|
+
${this.visitColumns()},
|
|
297
|
+
v.session_id,
|
|
298
|
+
ROW_NUMBER() OVER (
|
|
299
|
+
PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
|
|
300
|
+
) AS position,
|
|
301
|
+
COUNT(*) OVER (PARTITION BY v.session_id) AS total
|
|
302
|
+
FROM activity_visits v
|
|
303
|
+
WHERE v.project_id = ${input.projectId}
|
|
304
|
+
AND v.actor_type = 'user'
|
|
305
|
+
AND v.session_id = ANY(${this.sql.array([...input.sessionIds])}::text[])
|
|
306
|
+
AND v.user_id = ${input.userId}
|
|
307
|
+
) ranked
|
|
308
|
+
WHERE position <= ${input.limit}
|
|
309
|
+
ORDER BY session_id, position
|
|
310
|
+
`
|
|
311
|
+
|
|
312
|
+
const bySession = new Map<string, { visits: number; latest: ActivityVisit[] }>()
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] }
|
|
315
|
+
session.latest.push(visitFromRow(row))
|
|
316
|
+
bySession.set(row.session_id, session)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The visits `v` active in the period, and only the account's when one is asked for. A visit
|
|
324
|
+
* lasts less than a day, so it started at most a day before the period: that bounds the index
|
|
325
|
+
* range.
|
|
326
|
+
*/
|
|
327
|
+
private overlapping(input: ActivityPeriodInput | ActivityCalendarInput) {
|
|
328
|
+
const user = input.userId === undefined ? this.sql`` : this.sql`AND v.user_id = ${input.userId}`
|
|
329
|
+
const earliestStart = new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS)
|
|
330
|
+
|
|
331
|
+
return this.sql`
|
|
332
|
+
v.project_id = ${input.projectId}
|
|
333
|
+
AND v.started_at >= ${earliestStart}
|
|
334
|
+
AND v.started_at < ${input.to}
|
|
335
|
+
AND v.last_seen_at >= ${input.from}
|
|
336
|
+
${user}
|
|
337
|
+
`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private visitColumns() {
|
|
341
|
+
return this.sql`
|
|
342
|
+
v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
|
|
343
|
+
v.interactions, v.attribution::text AS attribution
|
|
344
|
+
`
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function readSource(
|
|
349
|
+
sql: SQLClient,
|
|
350
|
+
input: AppendActivityInput
|
|
351
|
+
): Promise<ActivitySourceState | undefined> {
|
|
352
|
+
const [row] = await sql<SourceRow[]>`
|
|
353
|
+
SELECT
|
|
354
|
+
s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
|
|
355
|
+
s.window_start, s.window_count
|
|
356
|
+
FROM activity_sources s
|
|
357
|
+
LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
|
|
358
|
+
WHERE s.project_id = ${input.projectId}
|
|
359
|
+
AND s.actor_type = ${input.actor.type}
|
|
360
|
+
AND s.session_id = ${input.sessionId}
|
|
361
|
+
`
|
|
362
|
+
if (!row) {
|
|
363
|
+
return undefined
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// A deleted visit leaves its source behind; the next batch starts another visit.
|
|
367
|
+
return {
|
|
368
|
+
visitId: row.visit_id,
|
|
369
|
+
visitStartedAt: row.visit_started_at === null ? null : new Date(row.visit_started_at).getTime(),
|
|
370
|
+
lastSeenAt: new Date(row.last_seen_at).getTime(),
|
|
371
|
+
sequence: Number(row.sequence),
|
|
372
|
+
windowStart: new Date(row.window_start).getTime(),
|
|
373
|
+
windowCount: Number(row.window_count),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The session's page views and interactions with an id of the batch, to recognize retries. */
|
|
378
|
+
async function readStoredEvents(
|
|
379
|
+
sql: SQLClient,
|
|
380
|
+
input: AppendActivityInput
|
|
381
|
+
): Promise<StoredActivityEvent[]> {
|
|
382
|
+
const ids = input.events.map((event) => event.id)
|
|
383
|
+
const rows = await sql<
|
|
384
|
+
{ id: string; route: string; name: string | null; attribution: string | null }[]
|
|
385
|
+
>`
|
|
386
|
+
SELECT p.id, p.route, NULL::text AS name, p.attribution::text AS attribution
|
|
387
|
+
FROM activity_page_views p
|
|
388
|
+
JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
|
|
389
|
+
WHERE p.project_id = ${input.projectId}
|
|
390
|
+
AND p.id = ANY(${sql.array(ids)}::text[])
|
|
391
|
+
AND v.actor_type = ${input.actor.type}
|
|
392
|
+
AND v.session_id = ${input.sessionId}
|
|
393
|
+
UNION ALL
|
|
394
|
+
SELECT i.id, i.route, i.name, NULL::text AS attribution
|
|
395
|
+
FROM activity_interactions i
|
|
396
|
+
JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
|
|
397
|
+
WHERE i.project_id = ${input.projectId}
|
|
398
|
+
AND i.id = ANY(${sql.array(ids)}::text[])
|
|
399
|
+
AND v.actor_type = ${input.actor.type}
|
|
400
|
+
AND v.session_id = ${input.sessionId}
|
|
401
|
+
`
|
|
402
|
+
|
|
403
|
+
return rows.map((row) => {
|
|
404
|
+
if (row.name !== null) {
|
|
405
|
+
return { id: row.id, route: row.route, name: row.name }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const attribution = parseActivityAttribution(row.attribution)
|
|
409
|
+
return { id: row.id, route: row.route, ...(attribution ? { attribution } : {}) }
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface SourceRow {
|
|
414
|
+
readonly visit_id: string
|
|
415
|
+
readonly visit_started_at: Date | string | null
|
|
416
|
+
readonly last_seen_at: Date | string
|
|
417
|
+
readonly sequence: number
|
|
418
|
+
readonly window_start: Date | string
|
|
419
|
+
readonly window_count: number
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface VisitRow {
|
|
423
|
+
readonly id: string
|
|
424
|
+
readonly actor_type: string
|
|
425
|
+
readonly user_id: string | null
|
|
426
|
+
readonly grant_id: string | null
|
|
427
|
+
readonly started_at: Date | string
|
|
428
|
+
readonly last_seen_at: Date | string
|
|
429
|
+
readonly page_views: number
|
|
430
|
+
readonly interactions: number
|
|
431
|
+
readonly attribution: string | null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
interface SessionVisitRow extends VisitRow {
|
|
435
|
+
readonly session_id: string
|
|
436
|
+
readonly total: number | string
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
interface UserRow {
|
|
440
|
+
readonly user_id: string
|
|
441
|
+
readonly last_seen_at: Date | string
|
|
442
|
+
readonly visits: string
|
|
443
|
+
readonly page_views: string
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface SummaryRow {
|
|
447
|
+
readonly users: string
|
|
448
|
+
readonly visits: string
|
|
449
|
+
readonly shared_visits: string
|
|
450
|
+
readonly page_views: string
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
interface PageViewRow {
|
|
454
|
+
readonly id: string
|
|
455
|
+
readonly visit_id: string
|
|
456
|
+
readonly sequence: number
|
|
457
|
+
readonly route: string
|
|
458
|
+
readonly received_at: Date | string
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A page view or, with a name, an interaction. */
|
|
462
|
+
interface EventRow extends PageViewRow {
|
|
463
|
+
readonly name: string | null
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function visitFromRow(row: VisitRow): ActivityVisit {
|
|
467
|
+
return {
|
|
468
|
+
id: row.id,
|
|
469
|
+
actor: activityActor(row.actor_type, row.user_id, row.grant_id),
|
|
470
|
+
attribution: parseActivityAttribution(row.attribution),
|
|
471
|
+
startedAt: new Date(row.started_at),
|
|
472
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
473
|
+
pageViews: Number(row.page_views),
|
|
474
|
+
interactions: Number(row.interactions),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function pageViewFromRow(row: PageViewRow): ActivityPageView {
|
|
479
|
+
return {
|
|
480
|
+
id: row.id,
|
|
481
|
+
visitId: row.visit_id,
|
|
482
|
+
sequence: Number(row.sequence),
|
|
483
|
+
route: row.route,
|
|
484
|
+
receivedAt: new Date(row.received_at),
|
|
485
|
+
}
|
|
486
|
+
}
|