Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/Analytics.tsx
2498 views
1
/**
2
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
8
import Cookies from "js-cookie";
9
import { v4 } from "uuid";
10
import { StartWorkspaceError } from "./start/StartPage";
11
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
12
13
export type Event =
14
| "invite_url_requested"
15
| "organisation_authorised"
16
| "dotfile_repo_changed"
17
| "feedback_submitted"
18
| "workspace_class_changed"
19
| "privacy_policy_update_accepted"
20
| "browser_extension_promotion_interaction"
21
| "coachmark_dismissed"
22
| "modal_dismiss"
23
| "ide_configuration_changed"
24
| "status_rendered"
25
| "error_rendered"
26
| "video_clicked"
27
| "waitlist_joined";
28
type InternalEvent = Event | "path_changed" | "dashboard_clicked";
29
30
export type EventProperties =
31
| TrackOrgAuthorised
32
| TrackInviteUrlRequested
33
| TrackDotfileRepo
34
| TrackFeedback
35
| TrackPolicyUpdateClick
36
| TrackBrowserExtensionPromotionInteraction
37
| TrackModalDismiss
38
| TrackIDEConfigurationChanged
39
| TrackWorkspaceClassChanged
40
| TrackStatusRendered
41
| TrackErrorRendered
42
| TrackVideoClicked
43
| TrackWaitlistJoined;
44
type InternalEventProperties = EventProperties | TrackDashboardClick | TrackPathChanged;
45
46
export interface TrackErrorRendered {
47
sessionId: string;
48
instanceId?: string;
49
workspaceId: string;
50
type: string;
51
error: any;
52
}
53
54
export interface TrackStatusRendered {
55
sessionId: string;
56
instanceId?: string;
57
workspaceId: string;
58
type: string;
59
phase?: string;
60
}
61
62
export interface TrackWorkspaceClassChanged {}
63
export interface TrackIDEConfigurationChanged {
64
location: string;
65
name?: string;
66
version?: string;
67
}
68
export interface TrackModalDismiss {
69
manner: string;
70
title?: string;
71
specify?: string;
72
path: string;
73
}
74
75
export interface TrackOrgAuthorised {
76
installation_id: string;
77
setup_action: string | undefined;
78
}
79
80
export interface TrackInviteUrlRequested {
81
invite_url: string;
82
}
83
84
export interface TrackDotfileRepo {
85
previous?: string;
86
current: string;
87
}
88
89
export interface TrackFeedback {
90
score: number;
91
feedback: string;
92
href: string;
93
path: string;
94
error_object?: StartWorkspaceError;
95
error_message?: string;
96
}
97
98
export interface TrackPolicyUpdateClick {
99
path: string;
100
success: boolean;
101
}
102
103
export interface TrackCoachmarkDismissed {
104
name: string;
105
success: boolean;
106
}
107
108
export interface TrackBrowserExtensionPromotionInteraction {
109
action: "chrome_navigation" | "firefox_navigation" | "manually_dismissed";
110
}
111
112
export interface TrackVideoClicked {
113
context: string;
114
path: string;
115
}
116
117
interface TrackDashboardClick {
118
dnt?: boolean;
119
path: string;
120
button_type?: string;
121
label?: string;
122
destination?: string;
123
}
124
125
interface TrackPathChanged {
126
prev: string;
127
path: string;
128
}
129
130
export interface TrackWaitlistJoined {
131
email: string;
132
feature: string;
133
}
134
135
interface Traits {
136
unsubscribed_onboarding?: boolean;
137
unsubscribed_changelog?: boolean;
138
unsubscribed_devx?: boolean;
139
}
140
141
//call this to track all events outside of button and anchor clicks
142
export function trackEvent(event: "invite_url_requested", properties: TrackInviteUrlRequested): void;
143
export function trackEvent(event: "organisation_authorised", properties: TrackOrgAuthorised): void;
144
export function trackEvent(event: "dotfile_repo_changed", properties: TrackDotfileRepo): void;
145
export function trackEvent(event: "feedback_submitted", properties: TrackFeedback): void;
146
export function trackEvent(event: "workspace_class_changed", properties: TrackWorkspaceClassChanged): void;
147
export function trackEvent(event: "privacy_policy_update_accepted", properties: TrackPolicyUpdateClick): void;
148
export function trackEvent(event: "coachmark_dismissed", properties: TrackCoachmarkDismissed): void;
149
export function trackEvent(
150
event: "browser_extension_promotion_interaction",
151
properties: TrackBrowserExtensionPromotionInteraction,
152
): void;
153
export function trackEvent(event: "modal_dismiss", properties: TrackModalDismiss): void;
154
export function trackEvent(event: "ide_configuration_changed", properties: TrackIDEConfigurationChanged): void;
155
export function trackEvent(event: "status_rendered", properties: TrackStatusRendered): void;
156
export function trackEvent(event: "error_rendered", properties: TrackErrorRendered): void;
157
export function trackEvent(event: "video_clicked", properties: TrackVideoClicked): void;
158
export function trackEvent(event: "waitlist_joined", properties: TrackWaitlistJoined): void;
159
export function trackEvent(event: Event, properties: EventProperties): void {
160
trackEventInternal(event, properties);
161
}
162
163
const trackEventInternal = (event: InternalEvent, properties: InternalEventProperties) => {
164
sendTrackEvent({
165
anonymousId: getAnonymousId(),
166
event,
167
properties,
168
});
169
};
170
171
// Please use trackEvent instead of this function
172
export function sendTrackEvent(message: RemoteTrackMessage): void {
173
sendAnalytics("trackEvent", message);
174
}
175
176
export function trackVideoClick(context: string) {
177
trackEvent("video_clicked", {
178
context: context,
179
path: window.location.pathname,
180
});
181
}
182
183
export const trackButtonOrAnchor = (target: HTMLAnchorElement | HTMLButtonElement | HTMLDivElement) => {
184
//read manually passed analytics props from 'data-analytics' attribute of event target
185
let passedProps: TrackDashboardClick | undefined;
186
if (target.dataset.analytics) {
187
try {
188
passedProps = JSON.parse(target.dataset.analytics) as TrackDashboardClick;
189
if (passedProps.dnt) {
190
return;
191
}
192
} catch (error) {
193
log.debug(error);
194
}
195
}
196
197
let trackingMsg: TrackDashboardClick = {
198
path: window.location.pathname,
199
label: target.ariaLabel || target.textContent || undefined,
200
};
201
202
if (target instanceof HTMLButtonElement || target instanceof HTMLDivElement) {
203
//parse button data
204
if (target.classList.contains("secondary")) {
205
trackingMsg.button_type = "secondary";
206
} else {
207
trackingMsg.button_type = "primary"; //primary button is the default if secondary is not specified
208
}
209
//retrieve href if parent is an anchor element
210
if (target.parentElement instanceof HTMLAnchorElement) {
211
const anchor = target.parentElement as HTMLAnchorElement;
212
trackingMsg.destination = anchor.href;
213
}
214
}
215
216
if (target instanceof HTMLAnchorElement) {
217
const anchor = target as HTMLAnchorElement;
218
trackingMsg.destination = anchor.href;
219
}
220
221
const getAncestorProps = (curr: HTMLElement | null): TrackDashboardClick | undefined => {
222
if (!curr || curr instanceof Document) {
223
return;
224
}
225
const ancestorProps: TrackDashboardClick | undefined = getAncestorProps(curr.parentElement);
226
const currProps = JSON.parse(curr.dataset.analytics || "{}") as TrackDashboardClick;
227
return { ...ancestorProps, ...currProps };
228
};
229
230
const ancestorProps = getAncestorProps(target);
231
232
//props that were passed directly to the event target take precedence over those passed to ancestor elements, which take precedence over those implicitly determined.
233
trackingMsg = { ...trackingMsg, ...ancestorProps, ...passedProps };
234
235
trackEventInternal("dashboard_clicked", trackingMsg);
236
};
237
238
//call this when the path changes. Complete page call is unnecessary for SPA after initial call
239
export const trackPathChange = (props: TrackPathChanged) => {
240
trackEventInternal("path_changed", props);
241
};
242
243
type TrackLocationProperties = {
244
referrer: string;
245
path: string;
246
host: string;
247
url: string;
248
};
249
250
export const trackLocation = async (includePII: boolean) => {
251
const props: TrackLocationProperties = {
252
referrer: document.referrer,
253
path: window.location.pathname,
254
host: window.location.hostname,
255
url: window.location.href,
256
};
257
258
sendAnalytics("trackLocation", {
259
//if the user is authenticated, let server determine the id. else, pass anonymousId explicitly.
260
includePII: includePII,
261
anonymousId: getAnonymousId(),
262
properties: props,
263
});
264
};
265
266
export const identifyUser = async (traits: Traits) => {
267
sendAnalytics("identifyUser", {
268
anonymousId: getAnonymousId(),
269
traits: traits,
270
});
271
};
272
273
function sendAnalytics(operation: "trackEvent" | "trackLocation" | "identifyUser", message: any) {
274
fetch("/_analytics/" + operation, {
275
method: "POST",
276
headers: {
277
"Content-Type": "application/json",
278
},
279
body: JSON.stringify(message),
280
credentials: "include",
281
});
282
}
283
284
const getCookieConsent = () => {
285
return Cookies.get("gp-analytical") === "true";
286
};
287
288
const getAnonymousId = (): string | undefined => {
289
if (!getCookieConsent()) {
290
//we do not want to read or set the id cookie if we don't have consent
291
return;
292
}
293
let anonymousId = Cookies.get("ajs_anonymous_id");
294
if (anonymousId) {
295
return anonymousId.replace(/^"(.+(?="$))"$/, "$1"); //strip enclosing double quotes before returning
296
} else {
297
anonymousId = v4();
298
Cookies.set("ajs_anonymous_id", anonymousId, { domain: "." + window.location.hostname, expires: 365 });
299
}
300
return anonymousId;
301
};
302
303