Skip to content
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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

vyorkin
Copy link
Contributor

@vyorkin vyorkin commented Jan 16, 2025

No description provided.

@vyorkin vyorkin self-assigned this Jan 16, 2025
Copy link

github-actions bot commented Jan 16, 2025

📦 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', () =>
Copy link
Contributor

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()

Copy link
Contributor Author

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=
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Member

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?

Copy link
Contributor Author

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({
Copy link
Contributor

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);?

Copy link
Contributor Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this readClientid()?

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

@vyorkin vyorkin Jan 17, 2025

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

Copy link
Contributor Author

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,
Copy link
Contributor

@zerts zerts Jan 16, 2025

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?

Copy link
Contributor Author

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}`,
Copy link
Contributor

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?

Copy link
Contributor Author

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>
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated ✅

@vyorkin vyorkin marked this pull request as draft January 20, 2025 16:30
@vyorkin vyorkin removed the request for review from everdimension January 20, 2025 16:30
@vyorkin vyorkin force-pushed the feature/add-conversion-tracking-WLT-6076 branch from 96636f8 to b85827b Compare January 26, 2025 07:54
// 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()])
Copy link
Member

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

Copy link
Contributor Author

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

@vyorkin vyorkin marked this pull request as ready for review February 15, 2025 12:17
if (!userId) {
return;
}
googleAnalyticsApi.setSessionId(userId);
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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

@vyorkin vyorkin requested a review from zerts February 20, 2025 07:35
@@ -177,6 +185,7 @@ function trackAppEvents({ account }: { account: Account }) {
...addressActionAnalytics,
});
sendToMetabase('signed_transaction', params);
gaCollect(account, 'signed_transaction', params);
Copy link
Contributor

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);

Copy link
Contributor

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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants