blob: 92e9abe589ca3753dffba2bb0b2e60bdf3a42537 [file] [log] [blame] [edit]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// GA4 library. Provides functions to send data to GA4 endpoints and interfaces that defines the
// data schema (measurement, event, etc).
// Exception: valid use of snake_case for property name to match the json field name
/* eslint-disable @typescript-eslint/naming-convention */
import * as https from 'https';
import * as logger from '../logger';
const QUERY_STRING = '?measurement_id=G-HHSGJ8EXW0&api_secret=Am2AYuPcTnK1KtJJloeLRg';
export interface Measurement {
client_id: string;
events: Event[];
user_id?: string;
timestamp_micros?: number;
user_properties?: UserProperties;
non_personalized_ads?: boolean;
}
export interface Event {
name: string;
params?: { [key: string]: string | number | boolean | undefined } & { items?: Item[] };
timestamp_micros?: number;
}
export interface UserProperty<T extends string | number | boolean> {
value: T;
timestamp_micros?: number;
}
export interface UserProperties {
[key: string]: UserProperty<string | number | boolean> | undefined;
}
export interface Item {
[key: string]: string | number | boolean;
}
/**
* Send a measurement to GA4 endpoint
* @param measurement the Measurement object to send
*
* The send method is implemented based on the same named method defined in
* https://github.com/Dart-Code/Dart-Code/blob/c85490fdc8/src/extension/analytics.ts
*/
export async function send(measurement: Measurement, debugLevel = 0): Promise<void> {
if (debugLevel > 0) {
logger.debug('Sending GA4 analytics: ' + JSON.stringify(measurement));
}
const options: https.RequestOptions = {
headers: {
'Content-Type': 'application/json',
},
hostname: 'www.google-analytics.com',
method: 'POST',
path: (debugLevel > 0 ? '/debug/mp/collect' : '/mp/collect') + QUERY_STRING,
port: 443,
};
await new Promise<void>((resolve) => {
try {
const req = https.request(options, (resp) => {
if (debugLevel > 0) {
resp.on('data', (c: Buffer | string) => {
try {
const gaDebugResp = JSON.parse(c.toString());
if (gaDebugResp && gaDebugResp.validationMessages &&
gaDebugResp.validationMessages.length === 0) {
logger.debug('GA4 Sent OK!');
} else if (
gaDebugResp && gaDebugResp.validationMessages &&
gaDebugResp.validationMessages.length > 0) {
logger.debug(`Invalid GA4 hit: ${c?.toString()}`);
} else {
logger.debug(`Unexpected GA4 debug response: ${c?.toString()}`);
}
} catch (e) {
logger.error(`Error in GA4 debug response: ${c?.toString()}`);
}
});
}
if (!resp || !resp.statusCode || resp.statusCode < 200 || resp.statusCode > 300) {
logger.error(
`Failed to send analytics ${resp && resp.statusCode}: ${resp && resp.statusMessage}`);
}
resolve();
});
req.write(JSON.stringify(measurement));
req.on('error', (e) => {
handleError(e);
resolve();
});
req.end();
} catch (e) {
handleError(e);
resolve();
}
});
}
/**
* Handles error during sending the analytics
* @param e the captured error
*/
function handleError(e: any) {
console.log(`Failed to send analytics, disabling for session: ${e}`);
}
/**
* Returns timestamp in microseconds
*/
function getTimestampMicros(): number {
return Date.now() * 1000;
}
/**
* Create an Event with name and params.
* @param name Event name
* @param params Event parameters
*/
export function createEvent(
name: string,
params?: { [key: string]: string | number | boolean | undefined }): Event {
var event: Event = {
name: name,
timestamp_micros: getTimestampMicros()
};
if (params !== undefined) {
event.params = params;
}
return event;
}
/**
* Create a Measurement with client_id, events and user_properties
* @param client_id Client ID, usually the UUID
* @param events List of Event
* @param user_properties User properties
*/
export function createMeasurement(
client_id: string,
events: Event[],
user_properties?: UserProperties): Measurement {
var measurement: Measurement = {
client_id: client_id,
events: events,
timestamp_micros: getTimestampMicros(),
non_personalized_ads: true
};
if (user_properties !== undefined) {
measurement.user_properties = user_properties;
}
return measurement;
}