cocottetech/@platform/infrastructure/sql/migrations/0005_surface_metrics.sql
autocommit 1f035e9697 db(infrastructure): 🗃️ Add surface metrics tracking tables/columns for analytics in platform database
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-19 23:17:53 -07:00

91 lines
3.7 KiB
SQL

-- ============================================================================
-- 0005_surface_metrics.sql
-- ----------------------------------------------------------------------------
-- Per-surface raw metrics snapshots. Insert-only on each ingestion (no
-- UPDATE); deduplicate on read via DISTINCT ON (window).
--
-- Adapters write rows; analytics-dashboard panels read via DISTINCT ON to
-- pick the most-recently-fetched snapshot per (user, surface, kind, window).
--
-- Spec source: _engineering-surface-metrics.md §3.
-- ============================================================================
CREATE TYPE surface_metric_kind AS ENUM (
-- Discovery funnel
'profile_view',
'search_impression',
'search_rank',
'click_through',
-- Engagement funnel
'dm_inbound',
'dm_outbound',
'reply_rate',
-- Conversion funnel
'subscription_new',
'subscription_total',
'tip_amount',
'tip_count',
'booking_inquiry',
'booking_confirmed',
-- Revenue
'gross_revenue',
'net_revenue'
);
CREATE TYPE surface_metric_source AS ENUM (
'native_api', -- surface exposed an API endpoint
'native_scrape', -- adapter scraped the surface's own analytics page
'derived', -- computed from other rows (e.g. reply_rate from dm_in/dm_out)
'manual' -- Quinn entered manually
);
CREATE TABLE surface_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
org_id UUID NULL REFERENCES orgs(id) ON DELETE CASCADE,
surface TEXT NOT NULL, -- surface_kind value
metric_kind surface_metric_kind NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
window_end TIMESTAMPTZ NOT NULL,
value_numeric NUMERIC(18,4), -- counts, sums, rates
value_text TEXT, -- rank labels ("Boosted"), categorical
currency TEXT, -- ISO 4217 when monetary
source surface_metric_source NOT NULL,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
payload_jsonb JSONB, -- raw surface-side response for audit
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT surface_metrics_window_chk CHECK (window_end > window_start),
CONSTRAINT surface_metrics_value_chk CHECK (value_numeric IS NOT NULL OR value_text IS NOT NULL),
CONSTRAINT surface_metrics_currency_chk CHECK (
currency IS NULL
OR (currency ~ '^[A-Z]{3}$' AND value_numeric IS NOT NULL)
)
);
CREATE INDEX idx_surface_metrics_user_surface_kind_window
ON surface_metrics (user_id, surface, metric_kind, window_end DESC);
CREATE INDEX idx_surface_metrics_org_surface_kind_window
ON surface_metrics (org_id, surface, metric_kind, window_end DESC)
WHERE org_id IS NOT NULL;
-- Dedup read: DISTINCT ON (surface, metric_kind, window_start, window_end)
-- ordered by fetched_at DESC picks the latest snapshot per window.
CREATE INDEX idx_surface_metrics_dedup
ON surface_metrics (user_id, surface, metric_kind, window_start, window_end, fetched_at DESC);
ALTER TABLE surface_metrics ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_surface_metrics ON surface_metrics
USING (
user_id = current_setting('app.user_id', true)::uuid
OR (org_id IS NOT NULL AND org_id = current_setting('app.org_id', true)::uuid)
)
WITH CHECK (
user_id = current_setting('app.user_id', true)::uuid
OR (org_id IS NOT NULL AND org_id = current_setting('app.org_id', true)::uuid)
);
COMMENT ON TABLE surface_metrics IS
'Per-surface metric snapshots. Insert-only; reads use DISTINCT ON to dedup overlapping windows. Tier-gated surfaces (e.g. Tryst Basic) simply contribute no rows — UI shows "—" not "0".';