History

Backfill last_seen_at from connection activity too (#1493)

Rhys Sullivan committed 3 months ago

Commit 8369cdd1 parent c333ea21 file · +32 −11Browse files

Unified · Split

apps/cloud/scripts/backfill-subjects.ts+32−11
@@ -14,13 +14,17 @@ · 13 unmodified lines
1414 //
1515 // created_at ← the tenant+subject's oldest connection, the closest honest
1616 // answer to "when did this user first appear".
17−// last_seen_at ← NULL. The column means "seen on a request", no request has
18−// been recorded, and the console renders NULL as "Never" —
19−// which is true since the sighting code deployed.
17+// last_seen_at ← the tenant+subject's newest connection `updated_at`, the
18+// latest activity storage can attest to. Rendering every
19+// pre-0012 user as "Never" was literally true of the sighting
20+// column but reads as "inactive user" to an operator, which
21+// is the wrong answer for a tenant full of active people. The
22+// first real sighting overwrites this with request truth.
2023 //
21−// Idempotent — ON CONFLICT on the (tenant, external_id) unique index leaves
22−// rows the runtime has since minted (or a prior run inserted) untouched, so
23−// re-running is safe and never rewinds a live row's fields.
24+// Idempotent — the insert's ON CONFLICT on the (tenant, external_id) unique
25+// index leaves rows the runtime has since minted (or a prior run inserted)
26+// untouched, and the last_seen_at repair only fills NULLs — so re-running is
27+// safe and never rewinds a live row's fields.
2428 // Pass --dry-run to print the plan without writing.
2529 // ---------------------------------------------------------------------------
2630
@@ -37,8 +41,12 @@ if (!connectionString) { · 10 unmodified lines
3741
3842 const sql = postgres(connectionString, { max: 1, prepare: false, ssl: "require" });
3943
40−const rows = await sql<{ tenant: string; subject: string; first_connection_at: Date }[]>`
41− SELECT tenant, subject, MIN(created_at) AS first_connection_at
44+const rows = await sql<
45+ { tenant: string; subject: string; first_connection_at: Date; last_activity_at: Date }[]
46+>`
47+ SELECT tenant, subject,
48+ MIN(created_at) AS first_connection_at,
49+ MAX(updated_at) AS last_activity_at
4250 FROM connection
4351 WHERE subject <> ''
4452 GROUP BY tenant, subject
@@ -47,20 +55,33 @@ const rows = await sql<{ tenant: string; subject: string; first_connection_at: D · 2 unmodified lines
4755 console.log(`${rows.length} (tenant, subject) pair(s) with connections`);
4856
4957 let inserted = 0;
58+let repaired = 0;
5059 for (const row of rows) {
5160 console.log(
52− `${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()}`,
61+ `${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()} last activity ${row.last_activity_at.toISOString()}`,
5362 );
5463 if (dryRun) continue;
64+ const lastSeenMs = row.last_activity_at.getTime();
5565 const result = await sql`
5666 INSERT INTO subject (row_id, tenant, external_id, created_at, last_seen_at, status)
57− VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, NULL, NULL)
67+ VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, ${lastSeenMs}, NULL)
5868 ON CONFLICT (tenant, external_id) DO NOTHING
5969 `;
6070 inserted += result.count;
71+ if (result.count > 0) continue;
72+ // The row predates this run but may still carry NULL from a run of the
73+ // pre-repair version of this script. Fill it in; a sighted row keeps its
74+ // request-truth timestamp.
75+ const repair = await sql`
76+ UPDATE subject SET last_seen_at = ${lastSeenMs}
77+ WHERE tenant = ${row.tenant} AND external_id = ${row.subject} AND last_seen_at IS NULL
78+ `;
79+ repaired += repair.count;
6180 }
6281
6382 console.log(
64− dryRun ? `dry run — would insert up to ${rows.length} row(s)` : `inserted ${inserted} row(s)`,
83+ dryRun
84+ ? `dry run — would insert up to ${rows.length} row(s)`
85+ : `inserted ${inserted} row(s), repaired last_seen_at on ${repaired} row(s)`,
6586 );
6687 await sql.end();