Device Data

Purpose and Scope

Device data is the analytics view that helps a Dub workspace understand how people interact with tracked links across hardware, browsers, operating systems, and trigger sources. In Dub terminology, this is not a separate product resource; it is a family of groupBy dimensions inside the Analytics API and the dashboard analytics experience. The first-party docs describe device analytics as a way to analyze users across different devices, browsers, and operating systems, with examples that retrieve clicks grouped by devices for a link over an interval.

Sources: apps/web/lib/openapi/analytics/index.ts, apps/web/lib/analytics/types.ts

This page focuses on the source-backed contract that connects that documentation to code. The OpenAPI module exposes a single GET /analytics operation, while the dashboard renders device-related groups through a reusable DeviceSection. The same family of dimensions appears in the typed analytics layer as DeviceTabs, which constrains the dashboard tabs to devices, browsers, os, and triggers. Together, these files define the supported reader workflow: choose an event such as clicks, pick a device-oriented grouping, optionally filter the dashboard by a specific value, and read a count or sales-based metric.

Sources: apps/web/ui/analytics/device-section.tsx, apps/web/lib/analytics/types.ts

Core Device Dimensions

Dub’s device analytics UI has four tabs: Devices, Browsers, OS, and Triggers. Devices summarize the client category, such as desktop, mobile, tablet, wearable, console, or smart TV. Browsers summarize the user agent’s browser family, with explicit handling for Chrome, Safari, Mobile Safari, Unknown, and a fallback image path for other browser names. OS summarizes the operating system, with special presentation for Mac OS, iOS, Unknown, and a generic fallback for other operating-system names. Triggers show how an event was initiated using the shared trigger display metadata.

Sources: apps/web/ui/analytics/device-section.tsx, apps/web/ui/analytics/device-icon.tsx

The UI separates a tab identifier from the singular filter key used in query parameters. DeviceSection starts with the devices tab, looks up the singular endpoint name from SINGULAR_ANALYTICS_ENDPOINTS, and then uses that singular name as the key for reading analytics rows and applying filters. This is why a row title is computed from d[singularTabName] for devices, browsers, and operating systems, while trigger rows read the display title from TRIGGER_DISPLAY. That distinction matters when debugging query-string behavior: the dashboard tab is plural, but the active filter key is singular.

Sources: apps/web/ui/analytics/device-section.tsx

The type layer keeps the dashboard vocabulary explicit. DeviceTabs is declared as the union devices | browsers | os | triggers, while AnalyticsGroupByOptions comes from the repository’s valid analytics endpoints. The broader AnalyticsFilters type is based on the Zod analytics query schema, then adds runtime fields such as workspaceId, dataAvailableFrom, and date objects for start and end. That means device analytics participates in the same analytics filtering contract as countries, referers, top links, and timeseries instead of relying on a one-off dashboard-only model.

Sources: apps/web/lib/analytics/types.ts

Dashboard Execution Flow

The dashboard route wrapper first checks whether the workspace can view analytics. AnalyticsClient loads workspace state with useWorkspace, shows LayoutLoader while that state is pending, and blocks the analytics area with WorkspaceExceededEvents when event usage has been exceeded. There is a special condition for Pro workspaces on an events page, but otherwise the wrapper is a gate around the analytics children. This source-level behavior aligns with the docs note that analytics endpoints require a paid plan tier, while keeping the exact dashboard behavior tied to workspace usage and plan state.

Sources: apps/web/app/app.dub.co/(dashboard)/[slug]/links/analytics/client.tsx

Once the analytics content is allowed to render, DeviceSection owns the local tab state and filter-selection state. It calls useAnalyticsFilterOption(tab) for the visible data and calls the same hook again with omitGroupByFilterKey: true for the full unfiltered data set. That lets the card display the current filtered list while also having access to all values for modal or comparison behavior. The component resets selected checkbox-like items whenever the user changes tabs so browser selections do not accidentally carry over to OS or trigger filtering.

Sources: apps/web/ui/analytics/device-section.tsx

Filtering is implemented through query-string mutation rather than hidden component state. When a user applies selected values, DeviceSection writes the singular analytics key to the URL with comma-separated values, or deletes the key when no values remain. It also derives isFilterActive from the current search parameters and can clear the filter by deleting that same key. This design makes device analytics shareable and recoverable from navigation: the URL represents the selected device, browser, OS, or trigger filter instead of only the in-memory React tree.

Sources: apps/web/ui/analytics/device-section.tsx

API Contract and Response Families

The public API surface is the GET /analytics path declared in the OpenAPI analytics module. Its operation id is retrieveAnalytics, it is tagged as Analytics, and SDK generation is guided by the Speakeasy name override retrieve. The operation summary says it retrieves analytics for a link, a domain, or the authenticated workspace. Its query parameters come from analyticsQuerySchema, and its security declaration requires token authentication. For device data, the important part is that the same operation can return arrays for AnalyticsDevices, AnalyticsBrowsers, AnalyticsOS, and AnalyticsTriggers.

Sources: apps/web/lib/openapi/analytics/index.ts

The response schema is intentionally a union because analytics has multiple shapes. Count analytics returns a count object, timeseries returns an array of time buckets, location views return continent, country, region, or city arrays, and device-oriented views return arrays whose schema names match the dashboard tabs. This is why client code must pay attention to the requested grouping and event rather than assuming a single response type. A request like event=clicks&groupBy=devices&linkId=...&interval=30d should be handled as a device-grouped array, while another grouping may return a different array shape.

Sources: apps/web/lib/openapi/analytics/index.ts

UI Icons and Value Presentation

The visual layer turns analytics strings into recognizable icons without changing the analytics contract. For the devices tab, DeviceIcon maps Desktop, Mobile, Tablet, Wearable, Console, and Smarttv to icons from @dub/ui/icons, defaulting to Desktop when the value is not recognized. For browsers, Chrome and Safari use first-party shared icons, Unknown uses a window icon, and the fallback loads a browser image based on the lowercase display value. For operating systems, Mac OS uses a Dub-hosted image, iOS uses an Apple logo, Unknown uses a cube, and other names fall back to UA Parser image assets.

Sources: apps/web/ui/analytics/device-icon.tsx

The card renders rows through BarList, mapping each analytics row into an icon, title, filter value, and numeric value. The numeric value is selected with dataKey: for the sales analytics tab it uses the current sale unit from context, otherwise it uses count. This matters because device analytics is not limited to click counts in the dashboard. The same grouping UI can rank devices or browsers by the selected metric family, and sales-oriented views can switch between sales count and monetary amount according to the analytics context.

Sources: apps/web/ui/analytics/device-section.tsx

Compact Reference

SurfaceConcrete namesBehavior
OpenAPI operationGET /analytics, retrieveAnalytics, SDK name retrieveRetrieves analytics for a link, domain, or authenticated workspace using analyticsQuerySchema.
Device response familiesAnalyticsDevices, AnalyticsBrowsers, AnalyticsOS, AnalyticsTriggersReturned as arrays in the analytics response union when the matching grouping is requested.
Dashboard tab type`DeviceTabs = "devices""browsers"
Dashboard cardDeviceSectionLoads grouped data, renders tabs, sorts values descending, and writes active filters into the URL.
Icon rendererDeviceIconConverts display strings into UI icons or fallback image assets for devices, browsers, operating systems, and triggers.
Utility entry pointapps/web/lib/analytics/utils/index.tsRe-exports analytics helpers including CSV conversion, export formatting, interval data, query-string editing, and plan date-range validation.

Sources: apps/web/lib/openapi/analytics/index.ts, apps/web/lib/analytics/types.ts, apps/web/ui/analytics/device-section.tsx, apps/web/ui/analytics/device-icon.tsx, apps/web/lib/analytics/utils/index.ts

Practical Usage

For API users, the main task is to call the analytics retrieval operation with a device-oriented groupBy value. The official docs show this pattern with SDK clients by setting event to clicks, groupBy to devices, adding a linkId, and choosing an interval such as 30d. The same conceptual shape applies when requesting browsers or operating systems, because the OpenAPI contract declares those response families in the same analytics operation. Consumers should branch on the requested grouping or generated SDK response type before rendering result rows.

Sources: apps/web/lib/openapi/analytics/index.ts

For dashboard contributors, the safest extension path is to preserve the separation between grouping, singular filter keys, and display values. New UI behavior should use DeviceTabs for tab identity, SINGULAR_ANALYTICS_ENDPOINTS for URL parameters, and DeviceIcon only for presentation. If you need export or reporting behavior, start from the analytics utility barrel because it already centralizes CSV conversion, export formatting, interval data, query-string editing, and plan-aware date-range validation. That keeps device analytics consistent with the rest of Dub’s analytics surface instead of forking a parallel path.

Sources: apps/web/lib/analytics/types.ts, apps/web/lib/analytics/utils/index.ts, apps/web/ui/analytics/device-section.tsx

Relevant Source Files

  • apps/web/ui/analytics/device-icon.tsx — maps device, browser, operating-system, and trigger display strings to icons or fallback image assets used in analytics rows.
  • apps/web/ui/analytics/device-section.tsx — implements the dashboard device analytics card, tab list, filter state, URL query mutation, data mapping, sorting, and sales-versus-count value selection.
  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/analytics/client.tsx — gates analytics page rendering on workspace loading, plan state, and exceeded-event behavior.
  • apps/web/lib/analytics/types.ts — defines analytics event, view, response, filter, sale-unit, group-by, and DeviceTabs types used by the analytics UI and API plumbing.
  • apps/web/lib/analytics/utils/index.ts — re-exports shared analytics helpers for CSV export, query-string editing, export formatting, interval data, and plan date-range validation.
  • apps/web/lib/openapi/analytics/index.ts — declares the GET /analytics OpenAPI operation and the response union that includes device, browser, OS, and trigger analytics arrays.

Next Steps

Read Analytics API next if you need the broader retrieval contract, event options, and response shapes. Read Analytics Overview if you want the bigger pipeline context before changing dashboard analytics. If your task is implementation-focused, trace DeviceSection first, then inspect the analytics query schema and response schemas that feed the OpenAPI union so UI changes remain compatible with generated SDK behavior.