| // 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; |
| } |