How to Build a Cross-Platform Analytics Dashboard: Google Analytics, Mixpanel, PostHog
June 30, 2026
Web and product analytics live in different platforms — Google Analytics for web traffic, Mixpanel and PostHog for product events, YouTube Analytics for video, Pendo for in-app adoption. Each has its own API, data model, metric definitions, and reporting structure. If your product surfaces analytics to customers, supporting more than one platform means building and maintaining a separate integration for each.
The Unified Analytics API normalizes analytics data into one model. Reports, properties, visitors, and events come back in a consistent shape regardless of source, so you build the dashboard once and let customers connect whichever platform they use.
This guide builds a cross-platform analytics dashboard: discover the properties a customer has connected, pull normalized reports with dimensioned metrics, and drill into visitors and events where the platform supports them.
Start with reports, not raw events
The most important design decision in a cross-platform analytics dashboard is what to build on.
Raw session and event data varies widely in availability across analytics platforms — some expose detailed session records, others only expose aggregated reporting. The one surface that is consistent across every supported platform is the Report: aggregated metrics over a date range, broken down by dimension.
A report's metrics array is the foundation. Each metric carries:
value— the numeric valuetype— what's being measured (USERS,SESSIONS,PAGE_VIEWS,CONVERSIONS,REVENUE,BOUNCE_RATE,VIDEO_VIEWS, and ~25 more)dimension— how it's broken down (DATE,SOURCE,COUNTRY,DEVICE_TYPE,PAGE,CAMPAIGN, and more)dimension_value— the specific value for that breakdown (e.g."US"for aCOUNTRYdimension)
That structure — value by type by dimension — is what powers every chart in a traffic dashboard, and it's available across Google Analytics, Mixpanel, PostHog, Pendo, YouTube Analytics, and additional platforms.
Step 1: Initialize the SDK
npm install @unified-api/typescript-sdk
import { UnifiedTo } from '@unified-api/typescript-sdk';
const sdk = new UnifiedTo({
security: { jwt: process.env.UNIFIED_API_KEY! },
});
const connectionId = process.env.UNIFIED_CONNECTION_ID!;
The connectionId identifies the authorized customer account — a connected analytics platform. Embed Unified's authorization component to handle the connection flow without building OAuth per platform.
Step 2: Discover connected properties
A property is the site, app, or analytics account being measured. Start by listing the properties available on the connection.
async function getProperties(connectionId: string) {
const properties = await sdk.analytics.listAnalyticsProperties({
connectionId,
limit: 100,
});
return (properties ?? []).map((p: any) => ({
id: p.id,
name: p.name,
timezone: p.timezone ?? null,
currency: p.currency ?? null,
}));
}
Properties give you the property_id values used to scope reports and other queries. id and name are returned across all supported platforms; timezone, currency, and industry are available where the platform provides them.
Step 3: Pull a report with dimensioned metrics
The report endpoint accepts a property, a date range, and optional type and dimension filters. This is the core dashboard query.
async function getReport(
connectionId: string,
propertyId: string,
startDate: string,
endDate: string,
metricType?: string,
dimension?: string
) {
const reports = await sdk.analytics.listAnalyticsReports({
connectionId,
property_id: propertyId,
start_gte: startDate, // ISO-8601
end_lt: endDate,
type: metricType ?? '', // e.g. 'SESSIONS', 'CONVERSIONS', 'REVENUE'
dimension: dimension ?? '', // e.g. 'SOURCE', 'COUNTRY', 'DATE'
limit: 100,
});
return reports ?? [];
}
The type and dimension parameters map to the normalized enums. For example, type: 'SESSIONS' with dimension: 'SOURCE' returns sessions broken down by acquisition source; type: 'PAGE_VIEWS' with dimension: 'COUNTRY' returns page views by country.
Step 4: Shape report metrics for charts
A report returns a metrics array. Each entry is a value tagged with its type, dimension, and dimension value. Transform that into chart-ready series.
type MetricPoint = {
label: string; // the dimension value, e.g. "google", "US", "2026-01-15"
value: number;
};
// Extract a single metric type, broken down by its dimension, sorted descending
function metricBreakdown(reports: any[], metricType: string): MetricPoint[] {
const points: MetricPoint[] = [];
for (const report of reports) {
for (const m of report.metrics ?? []) {
if (m.type === metricType && m.dimension_value != null) {
points.push({
label: m.dimension_value,
value: m.value ?? 0,
});
}
}
}
return points.sort((a, b) => b.value - a.value);
}
// Sum a metric type across all entries — for a single top-line number
function metricTotal(reports: any[], metricType: string): number {
let total = 0;
for (const report of reports) {
for (const m of report.metrics ?? []) {
if (m.type === metricType) total += m.value ?? 0;
}
}
return total;
}
Step 5: Build the dashboard views
Compose the report data into the views a traffic dashboard renders — top-line totals, an acquisition breakdown, and a geographic breakdown.
async function buildAnalyticsDashboard(
connectionId: string,
propertyId: string,
startDate: string,
endDate: string
) {
// Sessions broken down by source — for the acquisition chart
const sessionsBySource = await getReport(
connectionId, propertyId, startDate, endDate, 'SESSIONS', 'SOURCE'
);
// Page views broken down by country — for the geo map
const viewsByCountry = await getReport(
connectionId, propertyId, startDate, endDate, 'PAGE_VIEWS', 'COUNTRY'
);
// Sessions over time — for the trend line
const sessionsOverTime = await getReport(
connectionId, propertyId, startDate, endDate, 'SESSIONS', 'DATE'
);
return {
totals: {
sessions: metricTotal(sessionsBySource, 'SESSIONS'),
pageViews: metricTotal(viewsByCountry, 'PAGE_VIEWS'),
},
acquisitionBySource: metricBreakdown(sessionsBySource, 'SESSIONS'),
viewsByCountry: metricBreakdown(viewsByCountry, 'PAGE_VIEWS'),
sessionsTrend: metricBreakdown(sessionsOverTime, 'SESSIONS'),
};
}
// Usage
const dashboard = await buildAnalyticsDashboard(
connectionId,
'property_123',
'2026-01-01T00:00:00Z',
'2026-06-01T00:00:00Z'
);
console.log(`${dashboard.totals.sessions} sessions`);
console.log(`Top source: ${dashboard.acquisitionBySource[0]?.label}`);
Because the report model is normalized, this dashboard code runs the same whether the customer connected Google Analytics, Mixpanel, PostHog, Pendo, or YouTube Analytics. The metric types and dimensions are consistent; the platform differences are handled in the API layer.
Step 6: Drill into visitors and events where supported
For platforms that expose visitor-level and event-level detail, you can drill deeper. Visitor and event availability varies by platform, so treat these as enrichment on top of the report-based dashboard rather than its foundation.
Visitors — identity and engagement totals:
async function getTopVisitors(connectionId: string, propertyId: string) {
const visitors = await sdk.analytics.listAnalyticsVisitors({
connectionId,
property_id: propertyId,
limit: 50,
});
return (visitors ?? []).map((v: any) => ({
id: v.id,
name: v.name ?? null,
email: v.email ?? null,
country: v.country ?? null,
firstSeen: v.first_seen_at,
lastSeen: v.last_seen_at,
totalSessions: v.total_sessions ?? 0,
totalPageViews: v.total_page_views ?? 0,
totalEvents: v.total_events ?? 0,
}));
}
Events — individual tracked actions, filterable by type:
async function getConversionEvents(
connectionId: string,
propertyId: string,
startDate: string,
endDate: string
) {
const events = await sdk.analytics.listAnalyticsEvents({
connectionId,
property_id: propertyId,
type: 'PURCHASE', // or SIGN_UP, FORM_SUBMIT, etc.
start_gte: startDate,
end_lt: endDate,
limit: 100,
});
return (events ?? []).map((e: any) => ({
name: e.name,
type: e.event_type,
value: e.value ?? null,
currency: e.currency ?? null,
source: e.utm_source ?? null,
campaign: e.utm_campaign ?? null,
pageUrl: e.page_url ?? null,
}));
}
The event_type filter accepts the normalized enum — PAGE_VIEW, PURCHASE, SIGN_UP, FORM_SUBMIT, VIDEO_PLAY, and others — so conversion and funnel queries use the same vocabulary across platforms. Events with monetary value carry value and currency, which supports revenue and e-commerce reporting.
Why the unified model matters here
The dashboard code reads normalized values — metric type and dimension enums, total_sessions, event_type — not each platform's raw reporting schema. Google Analytics, Mixpanel, PostHog, Pendo, and YouTube Analytics each define metrics and dimensions differently. Unified maps them to one set of enums, so the dashboard logic is written once and works regardless of which platform a customer connects.
When you add another analytics platform, the dashboard doesn't change. Normalization happens in the API layer. Every read is a real-time, pass-through call to the source — Unified does not store the analytics data.
What Unified handles and what you own
Unified handles:
- Authorized connections to Google Analytics, Mixpanel, PostHog, Pendo, YouTube Analytics, and additional platforms
- A normalized analytics model — Property, Visitor, Session, Event, Report — with consistent metric types and dimensions across sources
- Real-time, pass-through reads with no stored data
You own:
- The dashboard UI and charting
- Any derived metrics or custom rollups
- The reporting period and refresh logic