Skip to content

Commit 37ad707

Browse files
CopilotLipata
andcommitted
Update Google Analytics implementation to GA4
Co-authored-by: Lipata <[email protected]>
1 parent 44f1a59 commit 37ad707

File tree

3 files changed

+114
-37
lines changed

3 files changed

+114
-37
lines changed

packages/core/types/GoogleAnalyticsParameters.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
export interface GoogleAnalyticsParameters {
22
/**
3+
* @deprecated No longer used in GA4. Kept for backwards compatibility.
34
* The tracking ID / web property ID. The format is UA-XXXX-Y.
4-
* All collected data is associated by this ID.
5+
* All collected data is associated by this ID.
56
*/
67
tid?: string;
78

8-
/** Application name. Should be always 'igniteui-cli'. */
9+
/**
10+
* @deprecated No longer used in GA4. Kept for backwards compatibility.
11+
* Application name. Should be always 'igniteui-cli'.
12+
*/
913
an?: string;
1014

1115
/** igniteui-cli application version. */
1216
av?: string;
1317

14-
/** User Agent. We will provide here node version as browser version and the user OS. */
18+
/**
19+
* @deprecated No longer used in GA4. Kept for backwards compatibility.
20+
* User Agent. We will provide here node version as browser version and the user OS.
21+
*/
1522
ua?: string;
1623

17-
/** User unique ID. */
24+
/**
25+
* @deprecated No longer used in GA4. Kept for backwards compatibility.
26+
* User unique ID.
27+
*/
1828
uid?: string;
1929

20-
/** The Protocol version. The current value is '1'. */
30+
/**
31+
* @deprecated No longer used in GA4. Kept for backwards compatibility.
32+
* The Protocol version. The current value is '1'.
33+
*/
2134
v?: number;
2235

2336
/**
2437
* The type of hit. Must be 'screenview', 'event' or 'exception'.
2538
* Use 'screenview' for each command.
2639
* Use 'event' for each user input.
2740
* Use 'exception' for exceptions.
41+
* This is mapped to GA4 event names.
2842
*/
2943
t?: string;
3044

packages/core/util/GoogleAnalytics.ts

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createHash} from "crypto";
22
import * as fs from "fs";
33
import * as path from "path";
4-
import * as qs from "querystring";
54
import { GoogleAnalyticsParameters } from "../types";
65
import { App } from "./App";
76
import { ProjectConfig } from "./ProjectConfig";
@@ -14,10 +13,12 @@ class GoogleAnalytics {
1413
protected static userSettings: string = "user-settings.json";
1514
protected static appVersion: string;
1615
protected static npmVersion: string;
17-
protected static trackingID = "UA-392932-23";
16+
// GA4 Measurement ID and API Secret
17+
protected static measurementID = "G-XXXXXXXXXX"; // TODO: Replace with actual GA4 Measurement ID
18+
protected static apiSecret = "XXXXXXXXXXXXXXXXXXXXXXXX"; // TODO: Replace with actual GA4 API Secret
1819

1920
/**
20-
* Generates http post request with provided parameters and sends it to GA
21+
* Generates http post request with provided parameters and sends it to GA4
2122
* @param parameters Object containing all the parameters to send
2223
*/
2324
public static post(parameters: GoogleAnalyticsParameters) {
@@ -26,46 +27,102 @@ class GoogleAnalytics {
2627
return;
2728
}
2829

29-
// set GA protocol version. This should be 1
30-
parameters.v = 1;
31-
32-
// set the Tracking ID
33-
parameters.tid = this.trackingID;
34-
35-
// set application version if not set beforehand
36-
if (!parameters.av) {
37-
if (!this.appVersion) {
38-
this.appVersion = Util.version();
39-
}
40-
41-
parameters.av = this.appVersion;
30+
// Get application version if not set
31+
if (!this.appVersion) {
32+
this.appVersion = Util.version();
4233
}
4334

44-
// set application name
45-
parameters.an = App.appName;
46-
47-
// set user agent string. We are using this for detecting the user's OS.
48-
// as well as node version. The latest is set as browser version.
35+
// Get user agent info
4936
const nodeVersion = process.version;
5037
const os = this.getOsForUserAgent();
5138
const npmVersion = this.getNpmVersion();
52-
parameters.ua = `node/${nodeVersion} (${os}) npm/${npmVersion}`;
53-
54-
// set user ID
55-
parameters.uid = this.getUUID();
39+
const userAgent = `node/${nodeVersion} (${os}) npm/${npmVersion}`;
40+
41+
// Get client ID (user ID)
42+
const clientId = this.getUUID();
43+
44+
// Build GA4 event payload
45+
const eventName = this.mapHitTypeToEventName(parameters.t || "event");
46+
const eventParams: any = {
47+
app_name: App.appName,
48+
app_version: parameters.av || this.appVersion,
49+
engagement_time_msec: "100" // Required for events
50+
};
51+
52+
// Map custom dimensions to event parameters
53+
if (parameters.cd) eventParams.screen_name = parameters.cd;
54+
if (parameters.ec) eventParams.event_category = parameters.ec;
55+
if (parameters.ea) eventParams.event_action = parameters.ea;
56+
if (parameters.el) eventParams.event_label = parameters.el;
57+
if (parameters.exd) eventParams.exception_description = parameters.exd;
58+
59+
// Map custom dimensions
60+
if (parameters.cd1) eventParams.framework = parameters.cd1;
61+
if (parameters.cd2) eventParams.project_type = parameters.cd2;
62+
if (parameters.cd3) eventParams.project_name = parameters.cd3;
63+
if (parameters.cd4) eventParams.action = parameters.cd4;
64+
if (parameters.cd5) eventParams.component_group = parameters.cd5;
65+
if (parameters.cd6) eventParams.component_name = parameters.cd6;
66+
if (parameters.cd7) eventParams.template_name = parameters.cd7;
67+
if (parameters.cd8) eventParams.custom_view_name = parameters.cd8;
68+
if (parameters.cd9) eventParams.extra_config = parameters.cd9;
69+
if (parameters.cd10 !== undefined) eventParams.skip_config = parameters.cd10;
70+
if (parameters.cd11 !== undefined) eventParams.skip_git = parameters.cd11;
71+
if (parameters.cd12 !== undefined) eventParams.global = parameters.cd12;
72+
if (parameters.cd13) eventParams.search_term = parameters.cd13;
73+
if (parameters.cd14) eventParams.theme = parameters.cd14;
74+
75+
// Build GA4 request payload
76+
const payload = {
77+
client_id: clientId,
78+
user_properties: {
79+
user_agent: {
80+
value: userAgent
81+
}
82+
},
83+
events: [{
84+
name: eventName,
85+
params: eventParams
86+
}]
87+
};
88+
89+
const payloadString = JSON.stringify(payload);
90+
const fullPath = `/mp/collect?measurement_id=${this.measurementID}&api_secret=${this.apiSecret}`;
91+
const options = {
92+
host: "www.google-analytics.com",
93+
path: fullPath,
94+
method: "POST",
95+
headers: {
96+
"Content-Type": "application/json",
97+
"Content-Length": Buffer.byteLength(payloadString)
98+
}
99+
};
56100

57-
// generate http request and sent it to GA
58-
const queryString = qs.stringify(parameters as {});
59-
const fullPath = "/collect?" + queryString;
60-
const options = { host: "www.google-analytics.com", path: fullPath, method: "POST" };
61101
const https = require("https");
62102
const req = https.request(options);
63103
req.on("error", e => {
64104
// TODO: save all the logs and send them later
65105
});
106+
req.write(payloadString);
66107
req.end();
67108
}
68109

110+
/**
111+
* Maps Universal Analytics hit types to GA4 event names
112+
*/
113+
protected static mapHitTypeToEventName(hitType: string): string {
114+
switch (hitType) {
115+
case "screenview":
116+
return "screen_view";
117+
case "event":
118+
return "cli_event";
119+
case "exception":
120+
return "exception";
121+
default:
122+
return "cli_event";
123+
}
124+
}
125+
69126
protected static getUUID(): string {
70127
const absolutePath = path.join(this.userDataFolder, this.appFolder, this.userSettings);
71128
let UUID = "";
@@ -74,7 +131,7 @@ class GoogleAnalytics {
74131
} else {
75132
const dirName = path.dirname(absolutePath);
76133
if (!fs.existsSync(dirName)) {
77-
fs.mkdirSync(dirName);
134+
fs.mkdirSync(dirName, { recursive: true });
78135
}
79136

80137
UUID = this.getUserID();

spec/unit/GoogleAnalytic-spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ describe("Unit - Google Analytic", () => {
1717
}
1818

1919
beforeEach(() => {
20-
request = jasmine.createSpyObj("request", ["on", "end"]);
20+
request = jasmine.createSpyObj("request", ["on", "end", "write"]);
2121
spyOn(https, "request").and.returnValue(request);
2222
serviceSpy =
2323
spyOn(child_process, "execSync").and.returnValue("some string which contains REG_SZ so we can get Machine Key");
2424
while (fs.existsSync(`./output/${testFolder}`)) {
2525
testFolder += 1;
2626
}
27-
fs.mkdirSync(`./output/${testFolder}`);
27+
fs.mkdirSync(`./output/${testFolder}`, { recursive: true });
2828
});
2929

3030
afterEach(() => {
@@ -41,6 +41,10 @@ describe("Unit - Google Analytic", () => {
4141
const requestOptions = (https.request as jasmine.Spy).calls.mostRecent().args[0];
4242
expect(requestOptions.host).toBe('www.google-analytics.com');
4343
expect(requestOptions.method).toBe('POST');
44+
expect(requestOptions.path).toContain('/mp/collect'); // GA4 endpoint
45+
expect(requestOptions.path).toContain('measurement_id=');
46+
expect(requestOptions.path).toContain('api_secret=');
47+
expect(requestOptions.headers['Content-Type']).toBe('application/json');
4448
expect(request.on).toHaveBeenCalledTimes(1);
4549
expect(request.on).toHaveBeenCalledWith("error", jasmine.any(Function));
4650

@@ -56,6 +60,8 @@ describe("Unit - Google Analytic", () => {
5660
const requestOptions = (https.request as jasmine.Spy).calls.mostRecent().args[0];
5761
expect(requestOptions.host).toBe('www.google-analytics.com');
5862
expect(requestOptions.method).toBe('POST');
63+
expect(requestOptions.path).toContain('/mp/collect'); // GA4 endpoint
64+
expect(requestOptions.headers['Content-Type']).toBe('application/json');
5965
expect(request.on).toHaveBeenCalledTimes(1);
6066
expect(request.on).toHaveBeenCalledWith("error", jasmine.any(Function));
6167

0 commit comments

Comments
 (0)