-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Track page views in Google Analytics #694
base: main
Are you sure you want to change the base?
Conversation
📦 build.zip [updated at Feb 18, 3:21:12 PM UTC] |
@@ -348,7 +356,13 @@ export function initialize({ account }: { account: Account }) { | |||
willSendRequest: createAddProviderHook({ getWalletProvider }), | |||
}); | |||
const handleUserId = () => mixpanelIdentify(account); | |||
account.on('authenticated', () => handleUserId()); | |||
|
|||
account.on('authenticated', () => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we can create a separate .on call for GA.
No need to combine them and probably no need to use Promise.allSettled()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, separate handler is better ✅
@@ -11,3 +11,5 @@ FEATURE_FOOTER_BUG_BUTTON=on | |||
FEATURE_SEND_FORM= | |||
MIXPANEL_TOKEN_PUBLIC= | |||
FEATURE_LOYALTY_FLOW=off | |||
GOOGLE_ANALYTICS_MEASUREMENT_ID= | |||
GOOGLE_ANALYTICS_API_SECRET= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This api key will be exposed. Probably it is ok, but everything that has secret in its name raises a concern
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Agree, it shouldn't cause much harm, but I'd prefer not to expose it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vyorkin Not sure what you mean, do you plan to remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Network traffic inspection could indeed reveal these GA API tokens... Could be solved by creating our custom endpoint, just like we did for access to firebase config. I'm just not sure it's worth it at this point
const ENDPOINT = 'https://www.google-analytics.com/mp/collect'; | ||
const DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100; | ||
|
||
function readClientid() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be an async function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated ✅
if (!clientId) { | ||
// Generate a unique client ID for Google Analytics V4. | ||
// The actual value is not relevant. | ||
clientId = crypto.randomUUID(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't we reuse userId that was generated for mixpanel here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Ok then
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a device id, why not reuse it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow, I missed it, thanks, this is exactly what I needed
|
||
export async function resetGoogleAnalyticsSessionId() { | ||
const sessionId = crypto.randomUUID() as string; | ||
await chrome.storage.session.set({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this also await browserStorage.set(CLIENT_ID_KEY, clientId);
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
local
and session
storage are different (according to docs). Probably we could use browserStorage (local
) here as well, but the google analytics v4 docs suggests using session storage. I think that is because the session storage is cleared if the extension is disabled, reloaded or updated and when the browser restarts
async function getSessionId() { | ||
// A sessionId represents a period of continuous user interaction with the extension. | ||
// Tracking it is necessary for Google Analytics V4. | ||
const result = await browser.storage.session.get(SESSION_ID_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this readClientid()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see the difference, shouldn't we use browserStorage.get<string>(SESSION_ID_KEY)
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If session storage is not written to disk, why use it? Why not just store this in a map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the implementation, so it is not relevant anymore
// A sessionId represents a period of continuous user interaction with the extension. | ||
// Tracking it is necessary for Google Analytics V4. | ||
const result = await browser.storage.session.get(SESSION_ID_KEY); | ||
let sessionId = result?.[SESSION_ID_KEY] as string | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct that we get result by SESSION_ID_KEY and then reading [SESSION_ID_KEY] in result?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we do the same thing in the webapis/storage.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not relevant anymore
if (event === 'page_view') | ||
return { | ||
page_title: document.title, | ||
page_location: document.location.href, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need .href
or .hash
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
google analytics docs suggests using .href
if (process.env.NODE_ENV !== 'development') { | ||
onIdle(() => { | ||
fetch( | ||
`${ENDPOINT}?measurement_id=${GOOGLE_ANALYTICS_MEASUREMENT_ID}&api_secret=${GOOGLE_ANALYTICS_API_SECRET}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be it will be a bit more readable to use URL() constructor here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not relevant anymore
|
||
export async function sendToGoogleAnalytics( | ||
event: GoogleAnalyticsEvent, | ||
extraParams: Record<string, unknown> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this extra params?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, but we may need them if we decide to send additional events to GA
|
||
if (process.env.NODE_ENV !== 'development') { | ||
onIdle(() => { | ||
fetch( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using ky
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated ✅
96636f8
to
b85827b
Compare
// Google Analytics does not have a clear definition of a user session. | ||
// Hence, we need to define what a user session means within the extension. | ||
// For simplicity, we initiate a new session each time the user authenticates. | ||
Promise.allSettled([handleUserId(), resetGoogleAnalyticsSessionId()]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get it, what does .allSettled
achieve here? It's not being awaited nor caught
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats odd, doesn't make sense to me either... Let me update this part
if (!userId) { | ||
return; | ||
} | ||
googleAnalyticsApi.setSessionId(userId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to store this sessionId if it is a userId and we have it on every event anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, while in mixpanel client it makes sense to store the user_id as a field within the API client class, GA api client is a bit simpler: we only have a single collect
method that requires session_id
, so we could just pass user_id
as a parameter each time it’s called
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated ✅
@@ -101,6 +102,13 @@ function trackAppEvents({ account }: { account: Account }) { | |||
mixpanelTrack(account, 'General: Screen Viewed', mixpanelParams); | |||
}); | |||
|
|||
emitter.on('screenView', async (data) => { | |||
gaCollect(account, 'page_view', { | |||
page_title: data.title, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need page title here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remember correctly, this is from GA docs, let me check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4
The page_view event also requires the page_title and page_location parameters
@@ -177,6 +185,7 @@ function trackAppEvents({ account }: { account: Account }) { | |||
...addressActionAnalytics, | |||
}); | |||
sendToMetabase('signed_transaction', params); | |||
gaCollect(account, 'signed_transaction', params); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may be we can introduce a helper prepareGaParams(account, params)
that will get a userId from account and add it to params. To make google-analytics service independent from Account class implementation. Not a big deal though.
@@ -348,12 +358,11 @@ export function initialize({ account }: { account: Account }) { | |||
willSendRequest: createAddProviderHook({ getWalletProvider }), | |||
}); | |||
const handleUserId = () => mixpanelIdentify(account); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we can revert changes here and in the account.on('reset') listener :)
Not a big deal, though
sendRequestsOverTheNetwork: process.env.NODE_ENV === 'production', | ||
}); | ||
|
||
Object.assign(globalThis, { googleAnalyticsApi }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this?
No description provided.