Files
Oliver Dunk 2862740483 Use <number>.<number> format for Google Analytics client ID (#1606)
* Use <number>.<number> format for Google Analytics client ID

With the `ENFORCE_RECOMMENDATIONS` [validation behavior](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=firebase#send_events_for_validation), Google Analytics warns about client IDs not in the <number>.<number> format.

This is not an issue in practice - that recommendation is for compatibility with existing client IDs and events are still processed with a client ID in any format. Additionally, the validation is not enabled by default.

However, this PR updates our sample code to use a consistent ID regardless to reduce noise if the validation is enabled. We use a random ID concatenated with a UNIX timestamp to match other GA client libraries.

* Run eslint
2026-01-07 15:19:18 +00:00

133 lines
4.5 KiB
JavaScript

const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
const GA_DEBUG_ENDPOINT = 'https://www.google-analytics.com/debug/mp/collect';
// Get via https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
const MEASUREMENT_ID = '<measurement_id>';
const API_SECRET = '<api_secret>';
const DEFAULT_ENGAGEMENT_TIME_MSEC = 100;
// Duration of inactivity after which a new session is created
const SESSION_EXPIRATION_IN_MIN = 30;
class Analytics {
constructor(debug = false) {
this.debug = debug;
}
getRandomId() {
const digits = '123456789'.split('');
let result = '';
for (let i = 0; i < 10; i++) {
result += digits[Math.floor(Math.random() * 9)];
}
return result;
}
// Returns the client id, or creates a new one if one doesn't exist.
// Stores client id in local storage to keep the same client id as long as
// the extension is installed.
async getOrCreateClientId() {
let { clientId } = await chrome.storage.local.get('clientId');
if (!clientId) {
// Generate a unique client ID, the actual value is not relevant. We use
// the <number>.<number> format since this is typical for GA client IDs.
const unixTimestampSeconds = Math.floor(new Date().getTime() / 1000);
clientId = `${this.getRandomId()}.${unixTimestampSeconds}`;
await chrome.storage.local.set({ clientId });
}
return clientId;
}
// Returns the current session id, or creates a new one if one doesn't exist or
// the previous one has expired.
async getOrCreateSessionId() {
// Use storage.session because it is only in memory
let { sessionData } = await chrome.storage.session.get('sessionData');
const currentTimeInMs = Date.now();
// Check if session exists and is still valid
if (sessionData && sessionData.timestamp) {
// Calculate how long ago the session was last updated
const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000;
// Check if last update lays past the session expiration threshold
if (durationInMin > SESSION_EXPIRATION_IN_MIN) {
// Clear old session id to start a new session
sessionData = null;
} else {
// Update timestamp to keep session alive
sessionData.timestamp = currentTimeInMs;
await chrome.storage.session.set({ sessionData });
}
}
if (!sessionData) {
// Create and store a new session
sessionData = {
session_id: currentTimeInMs.toString(),
timestamp: currentTimeInMs.toString()
};
await chrome.storage.session.set({ sessionData });
}
return sessionData.session_id;
}
// Fires an event with optional params. Event names must only include letters and underscores.
async fireEvent(name, params = {}) {
// Configure session id and engagement time if not present, for more details see:
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
if (!params.session_id) {
params.session_id = await this.getOrCreateSessionId();
}
if (!params.engagement_time_msec) {
params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC;
}
try {
const response = await fetch(
`${
this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT
}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: await this.getOrCreateClientId(),
events: [
{
name,
params
}
]
})
}
);
if (!this.debug) {
return;
}
console.log(await response.text());
} catch (e) {
console.error('Google Analytics request failed with an exception', e);
}
}
// Fire a page view event.
async firePageViewEvent(pageTitle, pageLocation, additionalParams = {}) {
return this.fireEvent('page_view', {
page_title: pageTitle,
page_location: pageLocation,
...additionalParams
});
}
// Fire an error event.
async fireErrorEvent(error, additionalParams = {}) {
// Note: 'error' is a reserved event name and cannot be used
// see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_names
return this.fireEvent('extension_error', {
...error,
...additionalParams
});
}
}
export default new Analytics();