feat: vista Orgánico con SEO (GA4/GSC) y GEO (visibilidad IA)
Nueva sección con métricas Search Console/GA4, cimientos SEO y Share of Model por motor (ChatGPT/Gemini/AI Overviews). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
75
sql/organic_geo.sql
Normal file
75
sql/organic_geo.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- Organic SEO (GA4/GSC) + GEO (AI visibility) warehouse
|
||||
-- Idempotent.
|
||||
|
||||
create table if not exists public.organic_daily (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
day date not null,
|
||||
gsc_clicks numeric not null default 0,
|
||||
gsc_impressions numeric not null default 0,
|
||||
gsc_ctr numeric,
|
||||
gsc_avg_position numeric,
|
||||
ga4_sessions numeric not null default 0,
|
||||
ga4_users numeric not null default 0,
|
||||
ga4_organic_sessions numeric not null default 0,
|
||||
ga4_conversions numeric not null default 0,
|
||||
source text not null default 'manual'
|
||||
check (source in ('manual', 'sync', 'import')),
|
||||
meta jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (tenant_id, day)
|
||||
);
|
||||
|
||||
create index if not exists organic_daily_tenant_day_idx
|
||||
on public.organic_daily (tenant_id, day desc);
|
||||
|
||||
create table if not exists public.geo_snapshots (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
label text not null default 'Medición GEO',
|
||||
period_start date,
|
||||
period_end date,
|
||||
-- Share of Model: % de primeras respuestas / menciones de la marca
|
||||
share_of_model numeric,
|
||||
-- Engines: { chatgpt, gemini, ai_overviews } visibility %
|
||||
engines jsonb not null default '{}'::jsonb,
|
||||
-- Official brand site citation share among sources
|
||||
official_citation_pct numeric,
|
||||
-- Sentiment -1..1 or 0..100 (store 0..100)
|
||||
sentiment_score numeric,
|
||||
-- Competitor share of model rows: [{ brand, share, rank }]
|
||||
competitors jsonb not null default '[]'::jsonb,
|
||||
-- Intent / category visibility: [{ category, visibility, rank }]
|
||||
categories jsonb not null default '[]'::jsonb,
|
||||
-- Top sources feeding AI answers
|
||||
top_sources jsonb not null default '[]'::jsonb,
|
||||
prompts_analyzed integer,
|
||||
responses_analyzed integer,
|
||||
study_url text,
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists geo_snapshots_tenant_created_idx
|
||||
on public.geo_snapshots (tenant_id, created_at desc);
|
||||
|
||||
alter table public.organic_daily enable row level security;
|
||||
alter table public.geo_snapshots enable row level security;
|
||||
|
||||
drop policy if exists organic_daily_tenant_select on public.organic_daily;
|
||||
create policy organic_daily_tenant_select on public.organic_daily
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists geo_snapshots_tenant_select on public.geo_snapshots;
|
||||
create policy geo_snapshots_tenant_select on public.geo_snapshots
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user