Skip to content

Queries ​

Example SQL queries for analyzing the data Do11y stores in Supabase. Events live in the do11y_events table with a payload jsonb column. You can run these via the Supabase SQL Editor or any PostgreSQL client.

PostgreSQL's JSONB operators extract fields from the payload: payload->>'field' returns text, and you cast to the appropriate type when needed (for example, ::numeric, ::boolean, ::timestamptz).

All field names follow OpenTelemetry semantic conventions. Event names use the browser.do11y.* namespace (e.g., browser.do11y.page_view) and attribute keys use OTel standard names where available (session.id, url.path, device.type).

Traffic and discovery ​

Entry points ​

Find the most common first pages users land on.

sql
select payload->>'url.path' as path, count(*) as entries
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
group by 1
order by entries desc
limit 20

Traffic sources ​

See where your visitors come from.

sql
select payload->>'browser.do11y.referrer_domain' as "referrerDomain", count(*) as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
group by 1
order by sessions desc

Entry point by referrer ​

Understand which sources land on which pages.

sql
select
    payload->>'browser.do11y.referrer_domain' as "referrerDomain",
    payload->>'url.path' as path,
    count(*) as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
group by 1, 2
order by sessions desc
limit 30

AI traffic overview ​

See how much of your documentation traffic comes from AI platforms.

sql
select
    count(*) as total,
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'ai') as "aiSessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'search-engine') as "searchSessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'direct') as "directSessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'social') as "socialSessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'community') as "communitySessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'code-host') as "codeHostSessions",
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'other') as "otherSessions",
    round(100.0 * count(*) filter (where payload->>'browser.do11y.referrer_category' = 'ai') / count(*), 1) as "aiPct"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true

AI traffic by platform ​

Break down AI traffic by platform (ChatGPT, Perplexity, Claude, etc.).

sql
select payload->>'browser.do11y.ai_platform' as "aiPlatform", count(*) as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
  and payload->>'browser.do11y.referrer_category' = 'ai'
group by 1
order by sessions desc

AI traffic trend ​

Track AI-referred sessions over time.

sql
select
    date_trunc('week', (payload->>'_time')::timestamptz) as week,
    count(*) as total,
    count(*) filter (where payload->>'browser.do11y.referrer_category' = 'ai') as ai,
    round(100.0 * count(*) filter (where payload->>'browser.do11y.referrer_category' = 'ai') / count(*), 1) as "aiPct"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
group by 1
order by week asc

Pages discovered via AI ​

Find which documentation pages AI platforms link to most.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.ai_platform' as "aiPlatform",
    count(*) as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
  and payload->>'browser.do11y.referrer_category' = 'ai'
group by 1, 2
order by sessions desc
limit 30

AI vs non-AI engagement ​

Compare engagement depth for AI-referred visitors vs other sources.

sql
select
    payload->>'browser.do11y.referrer_category' as "referrerCategory",
    count(*) as visits,
    avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgTime",
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll",
    avg((payload->>'browser.do11y.page_exit.engagement_ratio')::numeric) as "avgEngagement"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1
order by visits desc

Traffic source breakdown ​

Summarize traffic by category.

sql
select payload->>'browser.do11y.referrer_category' as "referrerCategory", count(*) as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and (payload->>'browser.do11y.is_first_page')::boolean = true
group by 1
order by sessions desc

Engagement and page performance ​

Page engagement score ​

Combine time, scroll depth, and engagement ratio into a single score.

sql
select
    path,
    "avgActiveTime",
    "avgEngagement",
    "avgScrollDepth",
    visits,
    ("avgActiveTime" * "avgEngagement" * "avgScrollDepth") / 100 as "engagementScore"
from (
    select
        payload->>'url.path' as path,
        avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgActiveTime",
        avg((payload->>'browser.do11y.page_exit.engagement_ratio')::numeric) as "avgEngagement",
        avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScrollDepth",
        count(*) as visits
    from do11y_events
    where payload->>'eventName' = 'browser.do11y.page_exit'
    group by 1
    having count(*) > 10
) sub
order by "engagementScore" desc

Scroll completion rate ​

Find pages where users read to the end.

sql
select
    path,
    total,
    completed,
    round(100.0 * completed / total, 1) as "completionRate"
from (
    select
        payload->>'url.path' as path,
        count(*) as total,
        count(*) filter (where (payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric >= 90) as completed
    from do11y_events
    where payload->>'eventName' = 'browser.do11y.page_exit'
    group by 1
    having count(*) > 10
) sub
order by "completionRate" desc

Bounce detection ​

Identify pages with high bounce rates (low scroll, low time).

sql
select
    payload->>'url.path' as path,
    avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgTime",
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll",
    count(*) as visits
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1
having count(*) > 5
  and avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) < 10
  and avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) < 25
order by visits desc

Where users get stuck ​

Exit pages ​

Find where sessions end (excluding single-page sessions).

sql
select "lastPage", count(*) as exits
from (
    select
        pages[array_length(pages, 1)] as "lastPage",
        pages[1] as "firstPage"
    from (
        select
            payload->>'session.id' as "sessionId",
            array_agg(payload->>'url.path' order by (payload->>'_time')::timestamptz) as pages
        from do11y_events
        where payload->>'eventName' = 'browser.do11y.page_view'
        group by 1
        having count(*) > 1
    ) sessions
) sub
where "firstPage" != "lastPage"
group by 1
order by exits desc
limit 20

Low engagement pages ​

Find high-traffic pages with poor engagement.

sql
select
    payload->>'url.path' as path,
    count(*) as visits,
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll",
    avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgTime"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1
having count(*) > 20
  and avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) < 30
order by visits desc

Pages with high search rate ​

Identify pages where users frequently open search (a potential confusion signal).

sql
select
    path,
    "totalViews",
    "sessionsWithSearch",
    round(100.0 * "sessionsWithSearch" / "totalViews", 1) as "searchRate"
from (
    select
        path,
        sum("pageViews") as "totalViews",
        count(*) filter (where searches > 0) as "sessionsWithSearch"
    from (
        select
            payload->>'session.id' as "sessionId",
            payload->>'url.path' as path,
            count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') as "pageViews",
            count(*) filter (where payload->>'eventName' = 'browser.do11y.search_opened') as searches
        from do11y_events
        group by 1, 2
        having count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') > 0
    ) per_session
    group by path
    having sum("pageViews") > 10
) per_page
order by "searchRate" desc

Page-to-page transitions ​

See how users move between pages.

sql
select
    payload->>'browser.do11y.previous_path' as "previousPath",
    payload->>'url.path' as path,
    count(*) as transitions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
  and payload->>'browser.do11y.previous_path' is not null
group by 1, 2
having count(*) > 5
order by transitions desc
limit 50

Journey depth distribution ​

Understand how many pages users view per session.

sql
select
    count(*) as sessions,
    avg("pageCount") as "avgPages",
    percentile_cont(0.5) within group (order by "pageCount") as "medianPages",
    percentile_cont(0.9) within group (order by "pageCount") as "p90Pages"
from (
    select payload->>'session.id' as "sessionId", count(*) as "pageCount"
    from do11y_events
    where payload->>'eventName' = 'browser.do11y.page_view'
    group by 1
) sub

Full session journeys ​

View complete page sequences for multi-page sessions.

sql
select
    "sessionId",
    journey,
    array_length(journey, 1) as "journeyLength"
from (
    select
        payload->>'session.id' as "sessionId",
        array_agg(payload->>'url.path' order by (payload->>'_time')::timestamptz) as journey
    from do11y_events
    where payload->>'eventName' = 'browser.do11y.page_view'
    group by 1
    having count(*) >= 3
) sub
limit 100

Find the most popular links across all pages.

sql
select
    payload->>'browser.do11y.link.text' as "linkText",
    payload->>'browser.do11y.link.target_url' as "targetUrl",
    count(*) as clicks
from do11y_events
where payload->>'eventName' = 'browser.do11y.link_click'
group by 1, 2
order by clicks desc
limit 30

See where users go when they leave.

sql
select payload->>'browser.do11y.link.target_url' as "targetUrl", count(*) as clicks
from do11y_events
where payload->>'eventName' = 'browser.do11y.link_click'
  and payload->>'browser.do11y.link.type' = 'external'
group by 1
order by clicks desc

Track specific CTAs (like "Sign up") by page and section.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.link.section' as "linkSection",
    count(*) as clicks
from do11y_events
where payload->>'eventName' = 'browser.do11y.link_click'
  and payload->>'browser.do11y.link.text' like '%Sign up%'
group by 1, 2
order by clicks desc

Find pages where users don't click links.

sql
select
    path,
    views,
    "linkClicks",
    round(100.0 * "linkClicks" / views, 1) as "clickRate"
from (
    select
        payload->>'url.path' as path,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') as views,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.link_click') as "linkClicks"
    from do11y_events
    group by 1
    having count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') > 20
) sub
order by "clickRate" asc

Code block engagement ​

Code copy rate by language ​

See which code languages users copy most.

sql
select payload->>'browser.do11y.code.language' as language, count(*) as copies
from do11y_events
where payload->>'eventName' = 'browser.do11y.code_copied'
group by 1
order by copies desc

Code copies by page ​

Find pages with the most code block engagement.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.code.section' as "codeSection",
    count(*) as copies
from do11y_events
where payload->>'eventName' = 'browser.do11y.code_copied'
group by 1, 2
order by copies desc
limit 30

Section reading patterns ​

Most-read sections ​

Find which headings users actually spend time reading.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.section.heading' as heading,
    count(distinct payload->>'session.id') as readers,
    avg((payload->>'browser.do11y.section.visible_seconds')::numeric) as "avgDwell"
from do11y_events
where payload->>'eventName' = 'browser.do11y.section_visible'
group by 1, 2
order by readers desc
limit 30

Skipped sections ​

Identify sections that users scroll past without reading.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.section.heading' as heading,
    count(distinct payload->>'session.id') as readers,
    avg((payload->>'browser.do11y.section.visible_seconds')::numeric) as "avgDwell"
from do11y_events
where payload->>'eventName' = 'browser.do11y.section_visible'
group by 1, 2
having avg((payload->>'browser.do11y.section.visible_seconds')::numeric) < 5
order by readers desc

Tab switch patterns ​

Most switched-to tabs ​

See which code language or framework tabs users select.

sql
select payload->>'browser.do11y.tab.label' as "tabLabel", count(*) as switches
from do11y_events
where payload->>'eventName' = 'browser.do11y.tab_switch'
  and (payload->>'browser.do11y.tab.is_default')::boolean = false
group by 1
order by switches desc

Tab switches by page ​

Understand audience preferences on specific pages.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.tab.label' as "tabLabel",
    payload->>'browser.do11y.tab.group' as "tabGroup",
    count(*) as switches
from do11y_events
where payload->>'eventName' = 'browser.do11y.tab_switch'
group by 1, 2, 3
order by switches desc
limit 30

Table of contents usage ​

Most clicked TOC entries ​

Find the headings users jump to via the on-page TOC.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.toc.heading' as heading,
    count(*) as clicks
from do11y_events
where payload->>'eventName' = 'browser.do11y.toc_click'
group by 1, 2
order by clicks desc
limit 30

Pages with heavy TOC usage ​

Identify pages where users rely on the TOC (a potential signal that the page is too long or poorly organized).

sql
select
    path,
    "tocClicks",
    views,
    round(100.0 * "tocClicks" / views, 1) as "tocRate"
from (
    select
        payload->>'url.path' as path,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.toc_click') as "tocClicks",
        count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') as views
    from do11y_events
    where payload->>'eventName' in ('browser.do11y.toc_click', 'browser.do11y.page_view')
    group by 1
    having count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') > 10
) sub
order by "tocRate" desc

User feedback ​

Feedback by page ​

See which pages get the best and worst ratings.

sql
select
    path,
    total,
    helpful,
    "notHelpful",
    round(helpful * 100.0 / total, 1) as "helpfulPct"
from (
    select
        payload->>'url.path' as path,
        count(*) as total,
        count(*) filter (where payload->>'browser.do11y.feedback.rating' = 'yes') as helpful,
        count(*) filter (where payload->>'browser.do11y.feedback.rating' = 'no') as "notHelpful"
    from do11y_events
    where payload->>'eventName' = 'browser.do11y.feedback'
    group by 1
    having count(*) >= 3
) sub
order by "helpfulPct" asc

Expand/collapse patterns ​

Most expanded sections ​

Find the <details> and accordion content users most want to see.

sql
select
    payload->>'url.path' as path,
    payload->>'browser.do11y.expand.summary' as summary,
    count(*) as expansions
from do11y_events
where payload->>'eventName' = 'browser.do11y.expand_collapse'
  and payload->>'browser.do11y.expand.action' = 'expand'
group by 1, 2
order by expansions desc
limit 30

Expand rate by page ​

Identify pages where users frequently expand hidden content.

sql
select
    path,
    expands,
    views,
    round(100.0 * expands / views, 1) as "expandRate"
from (
    select
        payload->>'url.path' as path,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.expand_collapse' and payload->>'browser.do11y.expand.action' = 'expand') as expands,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') as views
    from do11y_events
    where payload->>'eventName' in ('browser.do11y.expand_collapse', 'browser.do11y.page_view')
    group by 1
    having count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') > 10
) sub
order by "expandRate" desc

Content performance comparison ​

Page performance dashboard ​

View all page metrics in a single query.

sql
select
    path,
    "pageViews",
    "avgScrollDepth",
    "avgTimeSeconds",
    "linkClicks",
    "codeCopies",
    searches,
    "tocClicks",
    expands,
    round(1.0 * "linkClicks" / "pageViews", 2) as "clicksPerView",
    round(1.0 * "codeCopies" / "pageViews", 2) as "copiesPerView"
from (
    select
        payload->>'url.path' as path,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') as "pageViews",
        avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) filter (where payload->>'eventName' = 'browser.do11y.page_exit') as "avgScrollDepth",
        avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) filter (where payload->>'eventName' = 'browser.do11y.page_exit') as "avgTimeSeconds",
        count(*) filter (where payload->>'eventName' = 'browser.do11y.link_click') as "linkClicks",
        count(*) filter (where payload->>'eventName' = 'browser.do11y.code_copied') as "codeCopies",
        count(*) filter (where payload->>'eventName' = 'browser.do11y.search_opened') as searches,
        count(*) filter (where payload->>'eventName' = 'browser.do11y.toc_click') as "tocClicks",
        count(*) filter (where payload->>'eventName' = 'browser.do11y.expand_collapse') as expands
    from do11y_events
    group by 1
    having count(*) filter (where payload->>'eventName' = 'browser.do11y.page_view') > 10
) sub
order by "pageViews" desc

Compare sections ​

Aggregate performance by URL prefix (section).

sql
select
    substring(payload->>'url.path' from '^/([^/]+)') as section,
    count(*) as visits,
    avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgTime",
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1
order by visits desc

Week-over-week trend ​

Track traffic growth over time.

sql
select
    date_trunc('week', (payload->>'_time')::timestamptz) as week,
    count(*) as "pageViews",
    count(distinct payload->>'session.id') as "uniqueSessions"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
group by 1
order by week asc

Device and context ​

Mobile vs desktop engagement ​

Compare engagement by device type.

sql
select
    payload->>'device.type' as "deviceType",
    count(*) as visits,
    avg((payload->>'browser.do11y.page_exit.active_time_seconds')::numeric) as "avgTime",
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1

Viewport impact on engagement ​

See how screen size affects user behavior.

sql
select
    payload->>'browser.do11y.viewport_category' as "viewportCategory",
    count(*) as visits,
    avg((payload->>'browser.do11y.page_exit.max_scroll_depth')::numeric) as "avgScroll"
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_exit'
group by 1
order by visits desc

Browser breakdown ​

Understand your audience's browser preferences.

sql
select payload->>'browser.family' as "browserFamily", count(distinct payload->>'session.id') as sessions
from do11y_events
where payload->>'eventName' = 'browser.do11y.page_view'
group by 1
order by sessions desc

Key metrics reference ​

MetricGood signalWarning signal
Avg scroll depth> 60%< 25%
Avg active time30–120s< 10s
Completion rate (90% scroll)> 40%< 15%
Pages per session> 31 (bounce)
Search rate after page viewLowHigh (confusion)
Exit rateLow on guidesHigh on intro pages
TOC click rateLow (well-organized)High (page too long)
Feedback helpful %> 80%< 50%
Expand rateModerateVery high (promote content)
Section avg dwell> 10s< 3s (skipped)

Released under the MIT License.