App performance
We provide two out-of-the-box solutions for tracking app performance metrics:
- Plugins for automatic performance tracking on web
- Timing events, available in most Snowplow trackers, for manual tracking
Automatic tracking on web
The JavaScript tracker provides two configurable plugins that track performance automatically.
Use the web vitals plugin to track key user-centric performance metrics, and the performance navigation timing plugin to capture detailed navigation timing data.
Web vitals
This plugin tracks web performance metrics categorized as Web Vitals.
This table shows which versions of the trackers can use the web vitals plugins:
| Tracker | Supported | Since version | Auto-tracking | Notes |
|---|---|---|---|---|
| Web | ✅ | 3.13.0 | ✅ | |
| React Native | ❌ | Tracker can't access the required browser APIs | ||
| Google Tag Manager | ❌ | Not directly supported, but you can load it as an external plugin |
To process raw web vitals event data, use the core web vitals module in the Snowplow Unified Digital dbt package.
web_vitals
EventExample
{
"schema_name": "web_vitals",
"cls": 0.05,
"fcp": 0,
"fid": "36:00.0",
"inp": "36:00.0",
"lcp": 1908,
"navigationType": "navigate",
"ttfb": 228.9
}
Properties and schema
- Table
- JSON schema
| Property | Description |
|---|---|
cls (Cumulative Layout Shift)number | Optional. A unitless metric for measuring visual stability because it helps quantify how often users experience unexpected layout shifts. For more information https://web.dev/cls/. |
fid (First Input Delay)number | Optional. A metric for measuring load responsiveness because it quantifies the experience users feel when trying to interact with unresponsive pages. Measured in milliseconds. For more information https://web.dev/fid/. |
lcp (Largest Contentful Paint)number | Optional. A metric for measuring perceived load speed because it marks the point in the page load timeline when the page's main content has likely loaded. Measured in milliseconds. For more information https://web.dev/lcp/. |
fcp (First Contentful Paint)number | Optional. A metric for measuring perceived load speed because it marks the first point in the page load timeline where the user can see anything on the screen. Measured in milliseconds. For more information https://web.dev/fcp/. |
inp (Interaction to Next Paint)number | Optional. A metric that assesses responsiveness. INP observes the latency of all interactions a user has made with the page, and reports a single value which all (or nearly all) interactions were below that value. For more information https://web.dev/inp/. |
ttfb (Time To First Byte)number | Optional. A DOMHighResTimeStamp referring to the time in milliseconds between the browser requesting a page and when it receives the first byte of information from the server. For more information https://web.dev/ttfb/. |
navigationTypestring | Optional. The navigation type recognised from the Navigation Timing API https://www.w3.org/TR/navigation-timing-2/. E.g. 'navigate', 'reload', 'back-forward', 'back-forward-cache', 'prerender', 'restore'. |
{
"$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Schema for a web vitals tracking event. For more information on web vitals you can visit https://web.dev/vitals/.",
"self": {
"vendor": "com.snowplowanalytics.snowplow",
"name": "web_vitals",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"properties": {
"cls": {
"title": "Cumulative Layout Shift",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A unitless metric for measuring visual stability because it helps quantify how often users experience unexpected layout shifts. For more information https://web.dev/cls/."
},
"fid": {
"title": "First Input Delay",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A metric for measuring load responsiveness because it quantifies the experience users feel when trying to interact with unresponsive pages. Measured in milliseconds. For more information https://web.dev/fid/."
},
"lcp": {
"title": "Largest Contentful Paint",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A metric for measuring perceived load speed because it marks the point in the page load timeline when the page's main content has likely loaded. Measured in milliseconds. For more information https://web.dev/lcp/."
},
"fcp": {
"title": "First Contentful Paint",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A metric for measuring perceived load speed because it marks the first point in the page load timeline where the user can see anything on the screen. Measured in milliseconds. For more information https://web.dev/fcp/."
},
"inp": {
"title": "Interaction to Next Paint",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A metric that assesses responsiveness. INP observes the latency of all interactions a user has made with the page, and reports a single value which all (or nearly all) interactions were below that value. For more information https://web.dev/inp/."
},
"ttfb": {
"title": "Time To First Byte",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp referring to the time in milliseconds between the browser requesting a page and when it receives the first byte of information from the server. For more information https://web.dev/ttfb/."
},
"navigationType": {
"type": [
"string",
"null"
],
"maxLength": 128,
"description": "The navigation type recognised from the Navigation Timing API https://www.w3.org/TR/navigation-timing-2/. E.g. 'navigate', 'reload', 'back-forward', 'back-forward-cache', 'prerender', 'restore'."
}
},
"additionalProperties": false
}
Warehouse query
- Snowflake
- BigQuery
- Databricks
- Redshift & Postgres
select
unstruct_event_com_snowplowanalytics_snowplow_web_vitals_1 web_vitals_1
from
atomic.events
where
events.collector_tstamp > getdate() - interval '1 hour'
and events.event = 'unstruct'
and events.event_name = 'web_vitals'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
select
unstruct_event_com_snowplowanalytics_snowplow_web_vitals_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'web_vitals'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
select
unstruct_event_com_snowplowanalytics_snowplow_web_vitals_1
from
atomic.events events
where
events.collector_tstamp > timestampadd(HOUR, -1, current_timestamp())
and events.event = 'unstruct'
and events.event_name = 'web_vitals'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
and unstruct_event_com_snowplowanalytics_snowplow_web_vitals_1 is not null
select
"web_vitals_1".*
from
atomic.events events
join atomic.com_snowplowanalytics_snowplow_web_vitals_1 "web_vitals_1"
on "web_vitals_1".root_id = events.event_id and "web_vitals_1".root_tstamp = events.collector_tstamp
where
events.collector_tstamp > getdate() - interval '1 hour'
and "web_vitals_1".root_tstamp > getdate() - interval '1 hour'
and events.event = 'unstruct'
and events.event_name = 'web_vitals'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
Performance navigation timing
This plugin will add Performance Navigation Timing entities to all tracked events.
This table shows which versions of the trackers can use the performance plugins:
| Tracker | Supported | Since version | Auto-tracking | Notes |
|---|---|---|---|---|
| Web | ✅ | 3.10.0 | ✅ | |
| React Native | ❌ | Tracker can't access the required browser APIs to use these plugins | ||
| Google Tag Manager | ✅ | v4 | ✅ | Integrates with the performance navigation timing plugin |
PerformanceNavigationTiming
EntityProperties and schema
- Table
- JSON schema
| Property | Description |
|---|---|
decodedBodySizeinteger | Optional. A number that is the size (in octets) received from the fetch (HTTP or cache) of the message body, after removing any applied content encoding. |
encodedBodySizeinteger | Optional. A number representing the size (in octets) received from the fetch (HTTP or cache), of the payload body, before removing any applied content encodings. |
redirectStartnumber | Optional. A DOMHighResTimeStamp that represents the start time of the fetch which initiates the redirect in milliseconds. |
redirectEndnumber | Optional. A DOMHighResTimeStamp immediately after receiving the last byte of the response of the last redirect in milliseconds. |
fetchStartnumber | Optional. A DOMHighResTimeStamp immediately before the browser starts to fetch the resource in milliseconds. |
domainLookupStartnumber | Optional. A DOMHighResTimeStamp immediately before the browser starts the domain name lookup for the resource in milliseconds. |
domainLookupEndnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the browser finishes the domain name lookup for the resource in milliseconds. |
connectStartnumber | Optional. A DOMHighResTimeStamp immediately before the browser starts to establish the connection to the server to retrieve the resource in milliseconds. |
secureConnectionStartnumber | Optional. A DOMHighResTimeStamp immediately before the browser starts the handshake process to secure the current connection in milliseconds. |
connectEndnumber | Optional. A DOMHighResTimeStamp immediately after the browser finishes establishing the connection to the server to retrieve the resource in milliseconds. |
requestStartnumber | Optional. A DOMHighResTimeStamp immediately before the browser starts requesting the resource from the server in milliseconds. |
responseStartnumber | Optional. A DOMHighResTimeStamp immediately after the browser receives the first byte of the response from the server in milliseconds. |
responseEndnumber | Optional. A DOMHighResTimeStamp immediately after the browser receives the last byte of the resource or immediately before the transport connection is closed in milliseconds, whichever comes first. |
unloadEventStartnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the current document's unload event handler starts in milliseconds. |
unloadEventEndnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the current document's unload event handler completes in milliseconds. |
domInteractivenumber | Optional. A DOMHighResTimeStamp representing the time immediately before the user agent sets the document's readyState to 'interactive' in milliseconds. |
domContentLoadedEventStartnumber | Optional. A DOMHighResTimeStamp representing the time immediately before the current document's DOMContentLoaded event handler starts in milliseconds. |
domContentLoadedEventEndnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the current document's DOMContentLoaded event handler completes in milliseconds. |
domCompletenumber | Optional. A DOMHighResTimeStamp representing the time immediately before the user agent sets the document's readyState to 'complete' in milliseconds. |
loadEventStartnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the current document's load event handler starts in milliseconds. |
loadEventEndnumber | Optional. A DOMHighResTimeStamp representing the time immediately after the current document's load event handler completes in milliseconds. |
entryTypestring | Optional. The string 'navigation'. |
redirectCountinteger | Optional. A number representing the number of redirects since the last non-redirect navigation in the current browsing context. |
typestring | Optional. A string representing the navigation type. Either 'navigate', 'reload', 'back_forward' or 'prerender'. |
workerStartnumber | Optional. Returns a DOMHighResTimeStamp immediately before dispatching the FetchEvent if a Service Worker thread is already running, or immediately before starting the Service Worker thread if it is not already running. If the resource is not intercepted by a Service Worker the property will always return 0. |
nextHopProtocolstring | Optional. A string representing the network protocol used to fetch the resource, as identified by the ALPN Protocol ID (RFC7301) |
transferSizeinteger | Optional. A number representing the size (in octets) of the fetched resource. The size includes the response header fields plus the response payload body. |
durationnumber | Optional. Returns a timestamp that is the difference between the loadEventEnd and startTime properties. |
activationStartnumber | Optional. If the document is prerendered, activationStart represents the time between when the prerender was started and the document was actually activated. |
deliveryTypestring | Optional. Expose information about how a resource was delivered e.g. resources which were delivered from the cache. |
serverTimingarray | Optional. Array of PerformanceServerTiming entries. |
{
"$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Schema for page navigation performance entity, based on the PerformanceNavigationTiming interface (see https://w3c.github.io/navigation-timing/)",
"self": {
"vendor": "org.w3",
"name": "PerformanceNavigationTiming",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"properties": {
"decodedBodySize": {
"type": [
"integer",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A number that is the size (in octets) received from the fetch (HTTP or cache) of the message body, after removing any applied content encoding."
},
"encodedBodySize": {
"type": [
"integer",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A number representing the size (in octets) received from the fetch (HTTP or cache), of the payload body, before removing any applied content encodings."
},
"redirectStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp that represents the start time of the fetch which initiates the redirect in milliseconds."
},
"redirectEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately after receiving the last byte of the response of the last redirect in milliseconds."
},
"fetchStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately before the browser starts to fetch the resource in milliseconds."
},
"domainLookupStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately before the browser starts the domain name lookup for the resource in milliseconds."
},
"domainLookupEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp representing the time immediately after the browser finishes the domain name lookup for the resource in milliseconds."
},
"connectStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately before the browser starts to establish the connection to the server to retrieve the resource in milliseconds."
},
"secureConnectionStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately before the browser starts the handshake process to secure the current connection in milliseconds."
},
"connectEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately after the browser finishes establishing the connection to the server to retrieve the resource in milliseconds."
},
"requestStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately before the browser starts requesting the resource from the server in milliseconds."
},
"responseStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately after the browser receives the first byte of the response from the server in milliseconds."
},
"responseEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "A DOMHighResTimeStamp immediately after the browser receives the last byte of the resource or immediately before the transport connection is closed in milliseconds, whichever comes first."
},
"unloadEventStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately after the current document's unload event handler starts in milliseconds."
},
"unloadEventEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately after the current document's unload event handler completes in milliseconds."
},
"domInteractive": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately before the user agent sets the document's readyState to 'interactive' in milliseconds."
},
"domContentLoadedEventStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately before the current document's DOMContentLoaded event handler starts in milliseconds."
},
"domContentLoadedEventEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately after the current document's DOMContentLoaded event handler completes in milliseconds."
},
"domComplete": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately before the user agent sets the document's readyState to 'complete' in milliseconds."
},
"loadEventStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately after the current document's load event handler starts in milliseconds."
},
"loadEventEnd": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A DOMHighResTimeStamp representing the time immediately after the current document's load event handler completes in milliseconds."
},
"entryType": {
"type": [
"string",
"null"
],
"maxLength": 128,
"description": "The string 'navigation'."
},
"redirectCount": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 64,
"description": "A number representing the number of redirects since the last non-redirect navigation in the current browsing context."
},
"type": {
"type": [
"string",
"null"
],
"maxLength": 32,
"description": "A string representing the navigation type. Either 'navigate', 'reload', 'back_forward' or 'prerender'."
},
"workerStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": -2147483647,
"description": "Returns a DOMHighResTimeStamp immediately before dispatching the FetchEvent if a Service Worker thread is already running, or immediately before starting the Service Worker thread if it is not already running. If the resource is not intercepted by a Service Worker the property will always return 0."
},
"nextHopProtocol": {
"type": [
"string",
"null"
],
"maxLength": 16,
"description": "A string representing the network protocol used to fetch the resource, as identified by the ALPN Protocol ID (RFC7301)"
},
"transferSize": {
"type": [
"integer",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "A number representing the size (in octets) of the fetched resource. The size includes the response header fields plus the response payload body."
},
"duration": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "Returns a timestamp that is the difference between the loadEventEnd and startTime properties."
},
"activationStart": {
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0,
"description": "If the document is prerendered, activationStart represents the time between when the prerender was started and the document was actually activated."
},
"deliveryType": {
"type": [
"string",
"null"
],
"maxLength": 128,
"description": "Expose information about how a resource was delivered e.g. resources which were delivered from the cache."
},
"serverTiming": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/serverTiming",
"description": "PerformanceServerTiming entry"
},
"description": "Array of PerformanceServerTiming entries."
}
},
"definitions": {
"serverTiming": {
"type": "object",
"properties": {
"duration": {
"description": "Duration of the measurement.",
"type": [
"number",
"null"
],
"maximum": 2147483647,
"minimum": 0
},
"name": {
"description": "The name of the measurement.",
"type": "string",
"maxLength": 4096
},
"description": {
"description": "A short description of the measurement.",
"type": [
"string",
"null"
],
"maxLength": 4096
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
Manual timing events
Track things like resource load times or other performance measurements using manual timing events.
This table shows the support for timing events across Snowplow tracker SDKs.
| Tracker | Supported | Since version |
|---|---|---|
| Web | ✅ | 3.0.0 |
| iOS | ✅ | 2.0.0 |
| Android | ✅ | 2.0.0 |
| React Native | ✅ | 0.1.0 |
| Flutter | ✅ | 0.1.0 |
| Roku | ❌ | |
| Java | ✅ | 0.8.0 |
| Golang | ✅ | 0.1.0 |
| Python | ❌ | |
| Ruby | ❌ | |
| PHP | ❌ | |
| .NET | ✅ | 1.0.0 |
| C++ | ✅ | 0.3.0 |
| Rust | ✅ | 0.1.0 |
| Unity | ✅ | 0.1.0 |
| Scala | ❌ | |
| Lua | ❌ | |
| Node.js | ❌ | |
| Google Tag Manager | ❌ |
You can still track timing data using trackers without built-in timing event support. Use custom events with the timing schema.
timing
EventProperties and schema
- Table
- JSON schema
| Property | Description |
|---|---|
categorystring | Required. |
variablestring | Required. |
timingnumber | Required. |
labelstring | Optional. |
{
"$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Schema for a user timing event",
"self": {
"vendor": "com.snowplowanalytics.snowplow",
"name": "timing",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"properties": {
"category": {
"type": "string"
},
"variable": {
"type": "string"
},
"timing": {
"type": "number"
},
"label": {
"type": "string"
}
},
"required": [
"category",
"variable",
"timing"
],
"additionalProperties": false
}
Warehouse query
- Snowflake
- BigQuery
- Databricks
- Redshift & Postgres
select
unstruct_event_com_snowplowanalytics_snowplow_timing_1 timing_1
from
atomic.events
where
events.collector_tstamp > getdate() - interval '1 hour'
and events.event = 'unstruct'
and events.event_name = 'timing'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
select
unstruct_event_com_snowplowanalytics_snowplow_timing_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'timing'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
select
unstruct_event_com_snowplowanalytics_snowplow_timing_1
from
atomic.events events
where
events.collector_tstamp > timestampadd(HOUR, -1, current_timestamp())
and events.event = 'unstruct'
and events.event_name = 'timing'
and events.event_vendor = 'com.snowplowanalytics.snowplow'
and unstruct_event_com_snowplowanalytics_snowplow_timing_1 is not null
select
"timing_1".*
from
atomic.events events
join atomic.com_snowplowanalytics_snowplow_timing_1 "timing_1"
on "timing_1".root_id = events.event_id and "timing_1".root_tstamp = events.collector_tstamp
where
events.collector_tstamp > getdate() - interval '1 hour'
and "timing_1".root_tstamp > getdate() - interval '1 hour'
and events.event = 'unstruct'
and events.event_name = 'timing'
and events.event_vendor = 'com.snowplowanalytics.snowplow'