Skip to content

feat: add the @databricks/app-analytics browser SDK - #611

Draft
ditadi wants to merge 1 commit into
mainfrom
stack/app-analytics/01-sdk
Draft

ditadi wants to merge 1 commit into
mainfrom
stack/app-analytics/01-sdk

Conversation

@ditadi

@ditadi ditadi commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Why the change

Databricks Apps can record server-side telemetry but nothing about what users do in the browser, so this adds @databricks/app-analytics, a small browser SDK that sends page views, actions, and Web Vitals as OpenTelemetry log records to a same-origin endpoint.

Special things to note

Change outline

The package keeps the event model, delivery, and the three capture sources in separate folders:

packages/app-analytics/
├── src/
│   ├── client.ts          # appAnalytics / createAppAnalytics: init, track, page, flush, shutdown
│   ├── index.ts           # public exports
│   ├── react/index.tsx    # <AppAnalytics /> configures the shared client from props
│   ├── action/            # track() → action records
│   ├── page/              # page_view records + History API / popstate observer
│   ├── autocapture/       # clicks and submits on [data-app-analytics-event] elements
│   ├── web-vitals/        # LCP, INP, CLS, FCP, TTFB via web-vitals
│   └── core/
│       ├── event.ts, context.ts, properties.ts, data-spec.ts   # event model, session, attribute names
│       ├── sampling.ts    # per-session sampling (hash of the session id)
│       ├── queue.ts, scheduler.ts, delivery.ts                 # bounded queue, 5 s flush, retry
│       ├── transport.ts   # batching, fetch / keepalive / sendBeacon
│       └── otlp-json.ts   # OTLP/HTTP JSON encoding
├── README.md
└── tsdown.config.ts       # ESM + CJS builds for "." and "./react"

The public surface is one client interface, a default shared client, and a React wrapper:

interface AppAnalyticsOptions {
  endpoint?: string;            // default "/_analytics/v1/logs", same origin only
  automaticPageViews?: boolean; // default true
  autocapture?: boolean;        // default false
  webVitals?: boolean;          // default false
  sampleRate?: number;          // 0..1, default 1
  beforeSend?: (event) => boolean | void;
  onDiagnostic?: (diagnostic) => void; // never includes event content
}

interface AppAnalyticsClient {
  init(options?): void;
  track(name, properties?): void;
  page(properties?): void;
  flush(): Promise<void>;
  shutdown(): Promise<void>;
}

export const appAnalytics: AppAnalyticsClient;        // default client
export function createAppAnalytics(): AppAnalyticsClient;
export function AppAnalytics(props: AppAnalyticsOptions): null; // "./react"

Every capture source feeds the same record and delivery path. Failures inside the SDK are swallowed so they never affect the host app:

track() / page() / autocapture / web vitals
  record(createEvent)
    sampledSessionId()          # session in sessionStorage, 30 min inactivity timeout
    createEvent(metadata)       # id, timestamp, page and browser context
    beforeSend(event)           # false drops the event
    delivery.enqueue(endpoint)  # bounded queue of 100
DeliveryPipeline
  scheduler: every 5 s, on pagehide, or when 25 records are queued
  prepareBatch                  # ≤ 25 records and ≤ 48 KiB per request
  encodeOtlp → fetch POST       # one retry after a timeout, network error, or retryable status
  on pagehide → keepalive fetch (sendBeacon only where fetch is missing)

Each request is a single OTLP resourceLogs entry. Record attributes live under databricks.app.analytics.*:

{
  "resourceLogs": [{
    "resource": { "attributes": [
      { "key": "telemetry.sdk.name", "value": { "stringValue": "@databricks/app-analytics" } },
      { "key": "telemetry.sdk.language", "value": { "stringValue": "webjs" } }
    ] },
    "scopeLogs": [{
      "logRecords": [{
        "eventName": "page_view",
        "attributes": [
          { "key": "databricks.app.analytics.event.name", "value": { "stringValue": "page_view" } },
          { "key": "databricks.app.analytics.session.id", "value": { "stringValue": "…" } },
          { "key": "databricks.app.analytics.page.path", "value": { "stringValue": "/orders" } }
        ]
      }]
    }]
  }]
}

Repo wiring: a new app-analytics vitest project (jsdom), knip entries, the web-vitals NOTICE and license entries, and a line in CLAUDE.md.

This pull request and its description were written by Isaac.

Port the App Analytics browser SDK into packages/app-analytics, without
accessibility auditing. It records action, page_view, and web_vital
events and sends them as OTLP/HTTP JSON log records to
/_analytics/v1/logs by default. The endpoint can be overridden.

The package ships a framework-agnostic client (appAnalytics,
createAppAnalytics) and a React entry point (<AppAnalytics />). Delivery
batches records, caps request size, retries with backoff, and flushes
with sendBeacon when the page is hidden.

Nothing in AppKit uses the package yet.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 36273868057 -R databricks/appkit -n appkit-template-0.78.0-pr.b743987-stack-app-analytics-01-sdk-611 -D appkit-pr-611 \
  && unzip -o "appkit-pr-611/appkit-template-0.78.0-pr.b743987-stack-app-analytics-01-sdk-611.zip" -d "appkit-pr-611" \
  && databricks apps init --template "appkit-pr-611"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant