Skip to content

Commit b3bbe3b

Browse files
committed
update legacy ua analytics to ga4
1 parent 75e45c2 commit b3bbe3b

4 files changed

Lines changed: 146 additions & 30 deletions

File tree

js/googleAnalytics.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"128": "icons/NewWindowWithTabsToRight-Icon@128px.png"
1313
},
1414
"permissions": [
15-
"contextMenus"
15+
"contextMenus",
16+
"storage"
1617
],
1718
"content_security_policy": {
1819
"extension_pages": "script-src 'self'; object-src 'self'"

src/google-analytics.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// See:
2+
// https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4
3+
// https://developers.google.com/analytics/devguides/collection/protocol/ga4
4+
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag
5+
// https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/tutorial.google-analytics
6+
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/scripts/google-analytics.js
7+
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/service-worker.js
8+
9+
const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
10+
const GA_DEBUG_ENDPOINT = 'https://www.google-analytics.com/debug/mp/collect';
11+
12+
// Get via https://analytics.google.com/analytics/web/
13+
const MEASUREMENT_ID = 'G-BCGJGLETYW';
14+
const API_SECRET = '_xmXa00ATSmnNWs2J2xadQ';
15+
const DEFAULT_ENGAGEMENT_TIME_MSEC = 100;
16+
17+
// Duration of inactivity after which a new session is created
18+
const SESSION_EXPIRATION_IN_MIN = 30;
19+
20+
class Analytics {
21+
constructor(debug = false) {
22+
this.debug = debug;
23+
}
24+
25+
// Returns the client id, or creates a new one if one doesn't exist.
26+
// Stores client id in local storage to keep the same client id as long as
27+
// the extension is installed.
28+
async getOrCreateClientId() {
29+
let { clientId } = await chrome.storage.local.get('clientId');
30+
if (!clientId) {
31+
// Generate a unique client ID, the actual value is not relevant
32+
clientId = self.crypto.randomUUID();
33+
await chrome.storage.local.set({ clientId });
34+
}
35+
return clientId;
36+
}
37+
38+
// Returns the current session id, or creates a new one if one doesn't exist or
39+
// the previous one has expired.
40+
async getOrCreateSessionId() {
41+
// Use storage.session because it is only in memory
42+
let { sessionData } = await chrome.storage.session.get('sessionData');
43+
const currentTimeInMs = Date.now();
44+
// Check if session exists and is still valid
45+
if (sessionData && sessionData.timestamp) {
46+
// Calculate how long ago the session was last updated
47+
const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000;
48+
// Check if last update lays past the session expiration threshold
49+
if (durationInMin > SESSION_EXPIRATION_IN_MIN) {
50+
// Clear old session id to start a new session
51+
sessionData = null;
52+
} else {
53+
// Update timestamp to keep session alive
54+
sessionData.timestamp = currentTimeInMs;
55+
await chrome.storage.session.set({ sessionData });
56+
}
57+
}
58+
if (!sessionData) {
59+
// Create and store a new session
60+
sessionData = {
61+
session_id: currentTimeInMs.toString(),
62+
timestamp: currentTimeInMs.toString()
63+
};
64+
await chrome.storage.session.set({ sessionData });
65+
}
66+
return sessionData.session_id;
67+
}
68+
69+
// Fires an event with optional params. Event names must only include letters and underscores.
70+
async fireEvent(name, params = {}) {
71+
// Configure session id and engagement time if not present, for more details see:
72+
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
73+
if (!params.session_id) {
74+
params.session_id = await this.getOrCreateSessionId();
75+
}
76+
if (!params.engagement_time_msec) {
77+
params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC;
78+
}
79+
80+
try {
81+
const response = await fetch(
82+
`${
83+
this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT
84+
}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
85+
{
86+
method: 'POST',
87+
body: JSON.stringify({
88+
client_id: await this.getOrCreateClientId(),
89+
events: [
90+
{
91+
name,
92+
params
93+
}
94+
]
95+
})
96+
}
97+
);
98+
if (!this.debug) {
99+
return;
100+
}
101+
console.log(await response.text());
102+
} catch (e) {
103+
console.error('Google Analytics request failed with an exception', e);
104+
}
105+
}
106+
107+
// Fire a page view event.
108+
async firePageViewEvent(pageTitle, pageLocation, additionalParams = {}) {
109+
return this.fireEvent('page_view', {
110+
page_title: pageTitle,
111+
page_location: pageLocation,
112+
...additionalParams
113+
});
114+
}
115+
116+
// Fire an error event.
117+
async fireErrorEvent(error, additionalParams = {}) {
118+
// Note: 'error' is a reserved event name and cannot be used
119+
// see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_names
120+
return this.fireEvent('extension_error', {
121+
...error,
122+
...additionalParams
123+
});
124+
}
125+
}
126+
127+
export default new Analytics();

src/service_worker.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Analytics from './google-analytics.js';
2+
13
/**
24
* Handles the extension's installation event.
35
* Sets up context menus.
@@ -6,7 +8,7 @@
68
* @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-create}
79
* @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu}
810
*/
9-
chrome.runtime.onInstalled.addListener(() => {
11+
chrome.runtime.onInstalled.addListener(async (details) => {
1012
const menuContexts = ["page"];
1113

1214
const menuRoot = chrome.contextMenus.create({
@@ -42,6 +44,10 @@ chrome.runtime.onInstalled.addListener(() => {
4244
id: aboutTheDeveloper.name,
4345
title: "About the Developer"
4446
});
47+
48+
await Analytics.fireEvent('extension_lifecycle', {
49+
reason: details.reason
50+
});
4551
});
4652

4753
/**
@@ -60,6 +66,11 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
6066
};
6167

6268
await handlers[info.menuItemId]?.(tab);
69+
70+
await Analytics.fireEvent('task_triggered', {
71+
source: 'contextMenu',
72+
task: info.menuItemId
73+
});
6374
});
6475

6576
/**
@@ -77,6 +88,11 @@ chrome.commands.onCommand.addListener(async (command, tab) => {
7788
};
7889

7990
await handlers[command]?.(tab);
91+
92+
await Analytics.fireEvent('task_triggered', {
93+
source: 'command',
94+
task: command
95+
});
8096
});
8197

8298
/**

0 commit comments

Comments
 (0)