Adds the read-side Umami integration queued in last week's website-analytics plan (Phases 1–6 of `docs/website-analytics-flesh-out-plan.md`): - Realtime panel polls Umami at 5s intervals; world map renders visitor origins via echarts + `public/world-map/echarts-world.json` topo. - Sessions list + session-detail-sheet drill-down (per-session event timeline pulled from `/api/v1/website-analytics`). - Weekly heatmap (day-of-week × hour-of-day) for engagement timing. - Metric-detail pages under `/[portSlug]/website-analytics/[metric]` for pageviews / referrers / events deep-dives. - Email-pixel write path: `/api/public/email-pixel/[sendId]` 1×1 GIF beacon backed by `email_open_tracking` (migration 0076); resolves inline on render in inbox. - Tracked-link redirect: `/q/[slug]` routes through `tracked_links` (migration 0077) and forwards to the canonical destination after logging the click. - Dashboard `website-glance-tile` now reads from the live Umami service instead of placeholder data. Deps: `@umami/node`, `echarts`, `echarts-for-react`, `@types/geojson`, `@types/topojson-client`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
import { pgTable, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
|
|
|
import { ports } from './ports';
|
|
import { user } from './users';
|
|
import { documentSends } from './brochures';
|
|
|
|
/**
|
|
* Phase 4c — tracked redirect links. A short URL `/q/<slug>` records a
|
|
* click and 302s the recipient on to `targetUrl`. The matching click
|
|
* row is fire-and-forget so the redirect stays snappy; an aggregate
|
|
* `clickCount` on the parent row keeps "was clicked at all" queries
|
|
* cheap.
|
|
*
|
|
* `sendId` is the optional link back to the originating outbound email
|
|
* — set when the link is minted via the email-composer flow so reps can
|
|
* see per-email click-throughs. Manual one-off short links leave it null.
|
|
*/
|
|
export const trackedLinks = pgTable(
|
|
'tracked_links',
|
|
{
|
|
id: text('id')
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
portId: text('port_id')
|
|
.notNull()
|
|
.references(() => ports.id),
|
|
slug: text('slug').notNull(),
|
|
targetUrl: text('target_url').notNull(),
|
|
sendId: text('send_id').references(() => documentSends.id, { onDelete: 'set null' }),
|
|
clickCount: integer('click_count').notNull().default(0),
|
|
firstClickedAt: timestamp('first_clicked_at', { withTimezone: true }),
|
|
lastClickedAt: timestamp('last_clicked_at', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
createdByUserId: text('created_by_user_id').references(() => user.id, { onDelete: 'set null' }),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('uniq_tracked_links_slug').on(t.slug),
|
|
index('idx_tracked_links_send').on(t.sendId),
|
|
index('idx_tracked_links_port').on(t.portId, t.createdAt),
|
|
],
|
|
);
|
|
|
|
/** Per-click log. Apple Mail privacy proxy will pre-fetch tracked link
|
|
* URLs the same way it does pixels — clicks from iOS users are
|
|
* over-counted. Standard email-tracking caveats apply. */
|
|
export const trackedLinkClicks = pgTable(
|
|
'tracked_link_clicks',
|
|
{
|
|
id: text('id')
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
trackedLinkId: text('tracked_link_id')
|
|
.notNull()
|
|
.references(() => trackedLinks.id, { onDelete: 'cascade' }),
|
|
portId: text('port_id')
|
|
.notNull()
|
|
.references(() => ports.id),
|
|
clickedAt: timestamp('clicked_at', { withTimezone: true }).notNull().defaultNow(),
|
|
userAgent: text('user_agent'),
|
|
referer: text('referer'),
|
|
},
|
|
(t) => [
|
|
index('idx_tlc_link').on(t.trackedLinkId, t.clickedAt),
|
|
index('idx_tlc_port').on(t.portId, t.clickedAt),
|
|
],
|
|
);
|
|
|
|
export type TrackedLink = typeof trackedLinks.$inferSelect;
|
|
export type NewTrackedLink = typeof trackedLinks.$inferInsert;
|
|
export type TrackedLinkClick = typeof trackedLinkClicks.$inferSelect;
|
|
export type NewTrackedLinkClick = typeof trackedLinkClicks.$inferInsert;
|