Analytics
A Bijection analytics component for Bijection.
git clone https://github.com/bijectionhq/analytics.git
cd analytics
git checkout fadf5b7a09051f36e9bc4860047e6afd4db7abcaBijection repository access is required. Request access.
Description
A Bijection analytics component for Bijection.
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/analytics. Based on Abdssamie/convex-analytics. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
A convex analytics component for Convex.
Explore the demo ↗Upstream overview
From the upstream package documentation.
A complete first-party analytics solution for Convex applications that tracks pageviews, custom events, and user identity directly into your own database. Provides a real-time HTTP ingestion endpoint, a React dashboard component with timeseries charts, and a lightweight browser SDK with auto-batching. Events are stored and processed entirely within your Convex deployment without requiring third-party services.
Use cases
From the upstream package documentation.
- Replace Google Analytics or Mixpanel when you need complete data ownership and want analytics data in the same database as your application data
- Track product usage metrics like button clicks, feature adoption, and user flows with custom event properties and real-time aggregation
- Build customer-facing analytics dashboards using the provided React components and dashboard API for SaaS applications or client reporting
- Monitor multi-tenant applications by creating separate analytics sites for each tenant or environment while keeping all data in one Convex deployment
- Comply with privacy regulations by keeping all user analytics data on your own infrastructure without external data sharing
How it works
From the upstream package documentation.
The component defines its own Convex tables for sites, visitors, sessions, events, and rollups, with a two-phase ingestion architecture. Raw events flow through an HTTP endpoint that validates write keys and batches inserts, while a background worker processes pending events into aggregated visitors, sessions, and hourly/daily rollups for dashboard queries.
Setup involves installing the component with app.use(convexAnalytics), registering HTTP routes with registerRoutes(), and provisioning a site with provisionSite(). The browser SDK uses createAnalytics() to send batched events to the HTTP ingest endpoint, or you can load it via script tag with auto-initialization.
The dashboard API exposes read functions like getTimeseries(), getTopPages(), and getDashboardSummary() that you wrap with your own auth logic using exposeAnalyticsApi(). The React dashboard component renders timeseries charts and top-dimension breakdowns by consuming these authenticated queries through standard Convex useQuery() hooks.
Upstream package FAQ
How does Convex Analytics handle data privacy compared to third-party analytics?
Convex Analytics stores all data in your own Convex database, so no user data is sent to external services. You control data retention, access, and compliance. The write key acts like a publishable key for browser ingestion but requires authentication for dashboard access.
What's the difference between the HTTP ingest route and Convex mutations for tracking?
Convex Analytics uses HTTP ingest for browser events instead of public mutations for better performance and security. The browser SDK batches events and sends them to the HTTP route, which hashes the write key before calling internal component functions.
How does real-time aggregation work in the analytics component?
Raw events are inserted quickly with aggregatedAt: null, then a background worker processes them into visitors, sessions, and hourly/daily rollups. Dashboard queries use these materialized views for fast reporting while raw events remain available for detailed analysis.
Can I track multiple sites or apps with one Convex Analytics deployment?
Yes, Convex Analytics supports multiple sites through the sites table. Each site has its own write key and allowed origins. For single-site use cases, create one site named 'default' and ignore the multi-site features until needed.
What happens if my analytics write key gets leaked?
The write key is treated like a publishable key for ingestion only. Protect your deployment by setting allowedOrigins to restrict which domains can send events. Rotate the key by updating both your site record and frontend environment variables if needed.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Analytics with Bijection.
Read: https://bijection.com/components/analytics/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from Abdssamie/convex-analytics, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
Convex Analytics
A first-party, real-time product analytics component for Convex. Track pageviews, custom events, and user identity directly into your own database—no third-party services required.
- Real-time Ingestion: High-speed HTTP endpoint for event landing.
- Drop-in Dashboard: A complete React component with timeseries charts and metrics.
- First-party Data: You own the data; it stays in your Convex deployment.
- Browser SDK: Tiny, dependency-free client with auto-batching.
Quick Start
Before you start: one write key, two places
Generate one write key for your site.
You will use that same raw key in two places:
- server-side when creating the site
- browser-side when sending ingest requests
Suggested env vars:
- Convex env:
ANALYTICS_WRITE_KEY - frontend env:
NEXT_PUBLIC_ANALYTICS_WRITE_KEYorVITE_ANALYTICS_WRITE_KEY
If those two values do not match, events will not be ingested.
1. Install the Convex component
npm install @abdssamie/convex-analytics
import { defineApp } from "convex/server";
import convexAnalytics from "@abdssamie/convex-analytics/convex.config.js";
const app = defineApp();
app.use(convexAnalytics, { httpPrefix: "/analytics-component/" });
export default app;
2. Register the ingest route
// convex/http.ts
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@abdssamie/convex-analytics";
const http = httpRouter();
registerRoutes(http, components.convexAnalytics);
export default http;
If components.convexAnalytics is missing, start your Convex dev server first
so generated component bindings exist:
npx convex dev
3. Create the default site once
Create a tiny bootstrap mutation:
// convex/analytics.ts
import { components } from "./_generated/api";
import { provisionSite } from "@abdssamie/convex-analytics";
export const provisionDefaultSite = provisionSite(components.convexAnalytics, {
auth: async () => {},
site: {
slug: "default",
name: "Default site",
writeKey: process.env.ANALYTICS_WRITE_KEY!,
allowedOrigins: [], // Don't forget allowed origins here to protect your service if key gets leaked
},
});
Then run it once:
npx convex run analytics:provisionDefaultSite
provisionDefaultSite is idempotent. If default already exists, nothing breaks.
It also fails fast if your analytics write key env var is missing or empty.
4. Send events from the browser
import { createAnalytics } from "@abdssamie/convex-analytics";
const analytics = createAnalytics({
endpoint: "https://your-deployment.convex.site/analytics/ingest",
writeKey: process.env.NEXT_PUBLIC_ANALYTICS_WRITE_KEY!,
autoPageviews: false,
});
analytics.page();
analytics.track("signup_clicked", { plan: "pro" });
analytics.identify("user_123", { tier: "pro" });
That is enough to start sending analytics.
If your app is not bundling npm modules, you can load the tracker with a plain
script tag instead of importing createAnalytics(...) directly.
Auto-init with one script tag:
<script
defer
src="https://unpkg.com/@abdssamie/convex-analytics@latest/dist/embed/convex-analytics.js"
data-endpoint="https://your-deployment.convex.site/analytics/ingest"
data-write-key="write_..."
data-auto-pageviews="true"
></script>
Or initialize it manually:
<script src="https://unpkg.com/@abdssamie/convex-analytics@latest/dist/embed/convex-analytics.js"></script>
<script>
window.ConvexAnalytics.init({
endpoint: "https://your-deployment.convex.site/analytics/ingest",
writeKey: "write_...",
autoPageviews: false,
});
window.ConvexAnalytics.page();
</script>
Available browser globals:
window.ConvexAnalytics.init(...)window.ConvexAnalytics.page(...)window.ConvexAnalytics.track(...)window.ConvexAnalytics.identify(...)window.ConvexAnalytics.flush()
5. Render the dashboard
After the component and default site are set up, expose the read API from
your Convex backend and protect it with auth.
Backend: expose dashboard queries
// convex/analytics.ts
import { components } from "./_generated/api";
import {
exposeAdminApi,
exposeAnalyticsApi,
} from "@abdssamie/convex-analytics";
const auth = async (ctx: { auth: { getUserIdentity: () => Promise<unknown> } }) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthorized");
}
};
export const { getSiteBySlug } = exposeAdminApi(components.convexAnalytics, {
auth: async (ctx) => auth(ctx),
});
export const {
getDashboardSummary,
getOverview,
getTimeseries,
getTopPages,
getTopReferrers,
getTopSources,
getTopMediums,
getTopCampaigns,
getTopEvents,
getTopDevices,
getTopBrowsers,
getTopOs,
getTopCountries,
listRawEvents,
listPageviews,
listSessions,
listVisitors,
} = exposeAnalyticsApi(components.convexAnalytics, {
auth: async (ctx, operation) => {
await auth(ctx);
// Add your own site ownership check here for operation.siteId.
},
});
Frontend: render the dashboard
import { AnalyticsDashboard } from "@abdssamie/convex-analytics/react";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
export function DashboardPage() {
const site = useQuery(api.analytics.getSiteBySlug, { slug: "default" });
if (site === undefined) {
return <div>Loading...</div>;
}
if (site === null) {
return <div>Default analytics site not found.</div>;
}
return (
<AnalyticsDashboard
siteId={site._id}
api={{
getDashboardSummary: api.analytics.getDashboardSummary,
getOverview: api.analytics.getOverview,
getTimeseries: api.analytics.getTimeseries,
getTopPages: api.analytics.getTopPages,
getTopReferrers: api.analytics.getTopReferrers,
getTopSources: api.analytics.getTopSources,
getTopMediums: api.analytics.getTopMediums,
getTopCampaigns: api.analytics.getTopCampaigns,
getTopEvents: api.analytics.getTopEvents,
getTopDevices: api.analytics.getTopDevices,
getTopBrowsers: api.analytics.getTopBrowsers,
getTopOs: api.analytics.getTopOs,
getTopCountries: api.analytics.getTopCountries,
listRawEvents: api.analytics.listRawEvents,
listPageviews: api.analytics.listPageviews,
listSessions: api.analytics.listSessions,
listVisitors: api.analytics.listVisitors,
}}
/>
);
}
The dashboard itself does not handle authentication. Auth belongs in your backend wrappers, and the React component reads only from those authenticated queries.
Component stores only writeKeyHash, not the raw write key.
The browser write key is an ingest credential, not an admin secret. Treat it like
a publishable key: make it long and random, restrict allowedOrigins, and rotate
it if leaked.
What It Tracks
- Pageviews
- Custom product events
- Anonymous visitors
- Sessions
identify(userId, traits)links- Referrers and UTM campaign fields
- Top pages, events, referrers, and campaigns
- Overview and timeseries reports
Architecture
The component owns its own Convex tables:
sites: one tracked site/app per write keyvisitors: durable anonymous visitor recordssessions: session windows and coarse device/browser/country summaryevents: append-only raw events with lightweight aggregation markerrollups: hourly/daily report counters
flowchart LR
Browser["Browser SDK / HTTP client"] --> Http["HTTP ingest route"]
Http --> Ingest["ingestBatch\nappend-only raw event insert"]
Ingest --> Events[("events")]
Ingest --> Worker["reducePendingSiteEvents"]
Worker --> Visitors[("visitors")]
Worker --> Sessions[("sessions")]
Worker --> Rollups[("rollups\nhourly + daily")]
Rollups --> Dashboard["Dashboard queries\ngetOverview/getTimeseries/top lists"]
Events --> Dashboard
Sessions --> Dashboard
Why sites exists: one Convex deployment can track multiple sites or apps.
For the common one-site case, create one site named default and ignore the
multi-site parts until needed.
Browser traffic should use HTTP ingest route. Do not send every browser event through public Convex mutations. SDK batches events and HTTP route hashes write key before calling component.
Ingest and reporting are split on purpose. ingestBatch writes raw events
quickly, leaves aggregatedAt: null, and schedules background aggregation. The
worker materializes visitors, sessions, and rollups, then stamps
aggregatedAt.
Dashboard queries are range-aware:
- overview and top-dimension queries use exact edge handling
- first/last timeseries bucket is clipped to requested range
- raw events can lag by worker time, usually a few seconds
Troubleshooting
Browser shows a CORS error on /analytics/ingest
Example:
Access to fetch at 'https://your-deployment.convex.site/analytics/ingest'
from origin 'http://localhost:3001' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Check these first:
- the browser is using the same write key you used in
provisionDefaultSite - the site was created successfully
- the current origin is allowed for that site
Why this looks like CORS:
- the ingest route checks the write key and site before returning CORS headers
- if that check fails, the browser often reports a generic CORS error
Useful fixes:
npx convex run analytics:provisionDefaultSite
- verify your frontend env var matches
ANALYTICS_WRITE_KEY - verify
allowedOriginsincludes your local or production domain
components.convexAnalytics is missing
Run your Convex dev server first so generated component bindings exist:
npx convex dev
Dashboard says the default site is not found
You registered the component and queries, but the site record does not exist yet. Run:
npx convex run analytics:provisionDefaultSite
Dashboard API
When you want to read analytics in your app, expose only the read functions:
// convex/analytics.ts
import { components } from "./_generated/api";
import { exposeAnalyticsApi } from "@abdssamie/convex-analytics";
export const {
getDashboardSummary,
getOverview,
getTimeseries,
getEventPropertyBreakdown,
getTopPages,
getTopReferrers,
getTopSources,
getTopMediums,
getTopCampaigns,
getTopEvents,
getTopDevices,
getTopBrowsers,
getTopOs,
getTopCountries,
listRawEvents,
listPageviews,
listSessions,
listVisitors,
} = exposeAnalyticsApi(components.convexAnalytics, {
auth: async (ctx, operation) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthorized");
}
// Add your own site ownership check here for operation.siteId.
},
});
Dashboard surface:
getDashboardSummary(siteId, from, to, interval)getOverview(siteId, from, to)getTimeseries(siteId, from, to, interval)getEventPropertyBreakdown(siteId, eventName, propertyKey, from, to, limit?)getTopPages(siteId, from, to, limit?)getTopReferrers(siteId, from, to, limit?)getTopSources(siteId, from, to, limit?)getTopMediums(siteId, from, to, limit?)getTopCampaigns(siteId, from, to, limit?)getTopEvents(siteId, from, to, limit?)getTopDevices(siteId, from, to, limit?)getTopBrowsers(siteId, from, to, limit?)getTopOs(siteId, from, to, limit?)getTopCountries(siteId, from, to, limit?)listRawEvents(siteId, from?, to?, paginationOpts)listPageviews(siteId, from?, to?, paginationOpts)listSessions(siteId, from?, to?, paginationOpts)listVisitors(siteId, from?, to?, paginationOpts)
Advanced: Site Admin API
You do not need this for the initial install.
Use exposeAdminApi(...) only if your app needs site management:
import { components } from "./_generated/api";
import { exposeAdminApi } from "@abdssamie/convex-analytics";
export const {
createSite,
updateSite,
rotateWriteKey,
cleanupSite,
} = exposeAdminApi(components.convexAnalytics, {
auth: async (ctx, operation) => {
// admin auth / site ownership check here
},
});
Admin surface:
createSite,updateSite,rotateWriteKeycleanupSite(siteId? | slug?, now?, limit?, runUntilComplete?)
Recommended dashboard shape:
getOverviewfor KPI cardsgetTimeseriesfor chart- top-dimension queries for tables
getEventPropertyBreakdownfor lean product-event analysislistRawEvents/listPageviews/listSessions/listVisitorsfor drill-down/debug
Example product analytics query:
await ctx.runQuery(api.analytics.getEventPropertyBreakdown, {
siteId,
eventName: "plan_selected",
propertyKey: "plan",
from,
to,
limit: 10,
});
Recommended event naming:
track("plan_selected", { plan: "pro" })track("checkout_started", { step: "shipping" })identify(userId, { plan: "pro" })
Drill-down queries return Convex pagination objects:
pageisDonecontinueCursor
Raw events are append-only source of truth. Rollups are serving layer.
Retention
Retention is configured per site. Defaults are cost-conscious:
- raw
events: 90 days - hourly
rollups: 90 days - daily
rollups: kept indefinitely
retentionDays is shared default for raw events and hourly rollups. Override
specific fields when needed:
registerRoutes(http, components.convexAnalytics, {
path: "/analytics/ingest",
countryLookup: "headers-only",
});
Advanced: Cleanup and Retention
Cleanup is explicit so you control request volume and billing. For most apps, add one cleanup action and one cron and stop thinking about it.
If you want custom schedules, this is the manual shape:
// convex/cleanup.ts
import { components } from "./_generated/api";
import { internalAction } from "./_generated/server";
import { v } from "convex/values";
export const site = internalAction({
args: {
slug: v.string(),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
return await ctx.runAction(components.convexAnalytics.maintenance.cleanupSite, {
slug: args.slug,
limit: args.limit,
});
},
});
Then schedule them:
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval(
"analytics cleanup",
{ hours: 6 },
internal.cleanup.site,
{ slug: "default", limit: 100 },
);
export default crons;
These are admin functions. Do not call them from browser clients.
Use runUntilComplete: true only for one-off backfills or after downtime. For
normal production cron, keep it unset and let each run delete a bounded batch.
Browser SDK
Use the browser helper in your frontend:
import { createAnalytics } from "@abdssamie/convex-analytics";
const analytics = createAnalytics({
endpoint: "https://your-deployment.convex.site/analytics/ingest",
writeKey: "write_...",
autoPageviews: false,
flushIntervalMs: 5000,
maxBatchSize: 10,
});
analytics.page();
analytics.track("signup_clicked", { plan: "pro" });
analytics.identify("user_123", { tier: "pro" });
await analytics.flush();
The SDK stores:
visitorIdinlocalStoragesessionIdinsessionStorage- queued events in memory only
It flushes on interval, batch size, and pagehide.
autoPageviews: true works for traditional full-page loads. For SPAs, call
analytics.page() on route changes yourself:
useEffect(() => {
analytics.page();
}, [analytics, pathname]);
Cost Controls
The default ingest path is built to avoid unnecessary Convex usage:
- Browser events are batched.
- Raw events are slim.
- Write keys are hashed before reaching component storage.
- Ingest does not patch report counters inline.
- Reports use hourly/daily rollups for common analytics queries.
- Cleanup uses indexed, bounded batches and keeps daily rollups by default.
- Event properties can be allowlisted or denied per site.
- Raw IP addresses are not persisted by this component.
Note
While this component has been optimized to reduce convex usage cost, if you have many users or except to do so, then consider another service as convex-analytics hasn't been tested for large scale usage, but most developers don't have to bother about this.
Publishing
Release scripts:
npm run alphanpm run releasenpm run verify:release
Both go through preversion, which runs:
npm cinpm run build:cleannpm run testnpm run lintnpm run typecheck
npm run verify:release adds npm pack on top, matching CI.
The package version script is non-interactive now. It just formats
CHANGELOG.md and stages it if needed.
GitHub Actions automation:
CIruns on every push tomain, pull request, and manual dispatch.Publishruns onv*tags and manual dispatch.Publishrequires anNPM_TOKENrepository secret.- Tag-based publish verifies that
vX.Y.Zmatchespackage.jsonversionX.Y.Z.
Development
npm install
npm run build:codegen
npm test
npm run typecheck
Run the example app:
npm run dev
npm run dev:frontend
The example frontend is a small product app that sends analytics events. It is not a dashboard. Use the Convex dashboard to inspect component tables, functions, and stored events.