firebase / firebase-js-sdk

Firebase Javascript SDK
https://firebase.google.com/docs/web/setup
Other
4.84k stars 889 forks source link

FR: Very Important For Google Ads - Firebase Analytics App Instance Id for Web App #6881

Closed twittergadungan closed 6 months ago

twittergadungan commented 1 year ago

[REQUIRED] Describe your environment

[REQUIRED] Describe the problem

Steps to reproduce:

I am trying to log purchase event from my backend using Google Analytics 4 Measurement Protocol, if read this documentation, it is said that I must have app_instance_id so I can send the data from our backend to Google Analytics 4 server.

as you can read from that documentation, the app_instance_id is a unique identifier for a Firebase app instance. but the problem is I can't get that app_instance_id for my Web App instance ( I can get it easily for my Firebase Android and iOS app in Firebase Analytics module)

please provide the API to get the app_instance_id for Firebase Web App. it is very important, because if I can log purchase event or other events from my backend, I can associate that event to certain user so I can easily make custom audience (Purchaser) that can be exported to Google Ads so I can spend my money more effectively on Google Ads

hsubox76 commented 1 year ago

So app_instance_id is a mobile-only concept tied to the idea of an installed app, which web does not have.

However, my understanding from digging through Measurement Protocol documentation is that there client_id on web should serve the same purpose, but you have to send it to Measurement Protocol using the field client_id instead of app_instance_id.

Links (for my reference as well as yours): Measurement Protocol troubleshooting

Don't use advertising_id.

advertising_id is not supported as a valid device identifier. Use app_instance_id if you're using Firebase and client_id if you're using gtag.js."

This seems to indicate they both can serve the same purpose.

Given that, I suspect that part 2 of the gtag.js tab on the Measurement Protocol "sending events" page is applicable, since the web SDK version of Firebase Analytics is actually a wrapper around gtag.js.

So we will think about providing a method in the web SDK that returns the client_id. In the meantime, as a workaround, you should be able to get it using gtag (which should be available globally in your environment if you've initialized Firebase Analytics for web):

From: https://developers.google.com/tag-platform/gtagjs/reference#get_mp_example

gtag('get', 'UA-XXXXXXXX-Y', 'client_id', (clientID) => {
  sendOfflineEvent(clientID, "tutorial_begin")
});

function sendOfflineEvent(clientID, eventName, eventData) {
  // Send necessary data to your server...
}

The "UA-XXXXXX-Y" arg is the measurement ID which you should be able to get from the Firebase console page for your project.

If you can try using that workaround and tell me if it works, that would be really helpful and would give us the green light to add the official method (probably called getClientId()), which would basically be just that bit of code, wrapped in a promise and automatically supplying the measurementId.

twittergadungan commented 1 year ago

So app_instance_id is a mobile-only concept tied to the idea of an installed app, which web does not have.

However, my understanding from digging through Measurement Protocol documentation is that there client_id on web should serve the same purpose, but you have to send it to Measurement Protocol using the field client_id instead of app_instance_id.

Links (for my reference as well as yours): Measurement Protocol troubleshooting

Don't use advertising_id.

advertising_id is not supported as a valid device identifier. Use app_instance_id if you're using Firebase and client_id if you're using gtag.js."

This seems to indicate they both can serve the same purpose.

Given that, I suspect that part 2 of the gtag.js tab on the Measurement Protocol "sending events" page is applicable, since the web SDK version of Firebase Analytics is actually a wrapper around gtag.js.

So we will think about providing a method in the web SDK that returns the client_id. In the meantime, as a workaround, you should be able to get it using gtag (which should be available globally in your environment if you've initialized Firebase Analytics for web):

From: https://developers.google.com/tag-platform/gtagjs/reference#get_mp_example

gtag('get', 'UA-XXXXXXXX-Y', 'client_id', (clientID) => {
  sendOfflineEvent(clientID, "tutorial_begin")
});

function sendOfflineEvent(clientID, eventName, eventData) {
  // Send necessary data to your server...
}

The "UA-XXXXXX-Y" arg is the measurement ID which you should be able to get from the Firebase console page for your project.

If you can try using that workaround and tell me if it works, that would be really helpful and would give us the green light to add the official method (probably called getClientId()), which would basically be just that bit of code, wrapped in a promise and automatically supplying the measurementId.

@hsubox76

Hi Christina, thanks for the response. to test if a request is a valid GA4 Measurement protocol request we can use this Event Builder

please open this image: https://i.stack.imgur.com/4G7PL.png

as you can see, we can choose to use Firebase or gtag.js to send event to GA4 Measurement Protocol . in my case, I choose Firebase because I assume I can easily integrate the analytics to other Firebase products like remote config, FCM, in app messaging etc.

but unfortunately if I choose Firebase on that switch, then app_instance_id field is required. so if I send clientID to Measurement Protocol using the field client_id instead of app_instance_id then the Event Builder will see it as invalid event, because I have empty value on app_instance_id

as you can see from this documentation, app_instance_id is required if I want to send it using Firebase, it can't be empty or null

I can send the request to Measurement Protocol using gtag.js (instead of Firebase) but I am worried I will lose all Firebase integration benefits for my Web App

hsubox76 commented 1 year ago

I just want to double check, are you solely going off what the documentation/Event Builder says, or have you tried sending client_id instead of app_instance_id, and had it fail? I suspect the documentation may be incorrect, or at least misleading. I think the "firebase" switch might be for mobile Firebase SDKs only. As I mentioned above, the Firebase JS SDK actually uses gtag.js under the hood, so anything that works in gtag.js should theoretically work in the Firebase JS SDK. (Specifically the JS SDK only, the mobile SDKs do NOT use gtag.js.)

The fastest way to test it out would be to just send the client_id with gtag as above in a live app. However if you're really unsure that gtag.js and the Firebase JS SDK will behave the same, you can also send the client_id directly with the Firebase JS SDK.

logEvent(analytics, EVENT_NAME, { client_id: CLIENT_ID_HERE, /* any other params you are sending */ });
twittergadungan commented 1 year ago

@hsubox76

we just used event builder. but unfortunately, we can't find that gtag method so we can't get the clientID. maybe because our front end developer is currently using Nuxt JS Firebase for Vue Web App

hsubox76 commented 1 year ago

Can you clear up what you mean by "can't find that gtag method"? It's a global variable on window that should become available any time after getAnalytics() or initializeAnalytics() is run, it's not a method you import.

Although looking at your link, that library you are using doesn't support modular v9 Firebase (I would really recommend migrating to something that does, when you are able to), so I guess the equivalent to getAnalytics() in the compat version would be firebase.analytics(). Unfortunately the library you linked has an odd setup, so I don't know when in its process that happens. Apparently it can be set to lazy mode where it doesn't initialize until manually called? https://firebase.nuxtjs.org/guide/options#lazy I don't know, I don't think we can provide support for that library, but whenever it finishes calling the equivalent of firebase.analytics() is when you should be able to call the global gtag() function.

We can try to create a test app ourselves to see if this works but it may take some time. So you can wait for that if you are not in a rush, or if you can test this out yourself and verify the approach works, we can fast-track creating the new getClientId() method.

twittergadungan commented 1 year ago

@hsubox76

Sorry. I think I made a fatal mistake, I forgot to change api_secret and firebase_app_id as URL Parameters . I used the Android api_secret and firebase_app_id instead of using api_secret and firebase_app_id for web app. so unfortunately I can confirm using client_id as a replacement for app_instance_id does not work

hsubox76 commented 1 year ago

Ok, looks like we are going to have to do some testing and figure out what works on web, if anything. This may take some time, we will add updates in this issue when we discover anything new.

dwyfrequency commented 1 year ago

I was able to use the client_id from gtag with measurement protocol. @twittergadungan are you able to do the same? I am not sure the best way to see whether it is considered the same user in the Google Analytics dashboard but can see both events coming through. Note, it appears that MP events take much longer to propagate then do the events from gtag.

// gtag call
window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}
gtag("js", new Date());
const measurementId = "INSERT-ID-HERE";
gtag("set", { currency: "USD" });

gtag("event", "purchase", {
        client_id: "511285180.1674077316",
        user_id: "USERID1",
        timestamp_micros: "1674083595919000",
        non_personalized_ads: false,
        events: [
          {
            name: "purchase",
            params: {
              items: [
                { item_id: "ITEM_1", item_name: "coffee mugs", quantity: 1 },
              ],
              affiliation: "GOOGLE STORE",
            },
          },
        ],
      });

gtag("get", measurementId, "client_id", (clientID) => {
  console.log({ clientID }); // gets you the client id for MP
}); 
// MP call
POST https://www.google-analytics.com/mp/collect?api_secret=<INSERT-SECRET-HERE>&measurement_id=<INSERT-ID-HERE>
HOST: www.google-analytics.com
Content-Type: application/json

{"client_id":"<ID-FROM-GTAG-GET>","user_id":"USERID1","timestamp_micros":"1674152143864000","non_personalized_ads":false,"events":[{"name":"refund","params":{"items":[{"item_id":"ITEM_1","item_name":"coffee mugs","quantity":1}],"affiliation":"GOOGLE STORE"}}]}

Screenshot 2023-01-23 at 2 51 49 PM

abhi-gn commented 1 year ago

@hsubox76

It's a global variable on window that should become available any time after getAnalytics() or initializeAnalytics() is run

gtag('get', 'UA-XXXXXXXX-Y', 'client_id', (clientID) => {
  log.info(clientID)
});

I tried the above steps and was able to get the gtag instance too, however the get method never returns anything. The callback is never called.

Logging the window.gtag function to console, I get this:

async function gtagWrapper(command, idOrNameOrParams, gtagParams) {
    try {
      // If event, check that relevant initialization promises have completed.
      if (command === "event" /* GtagCommand.EVENT */) {
        // If EVENT, second arg must be measurementId.
        await gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams, gtagParams);
      } else if (command === "config" /* GtagCommand.CONFIG */) {
        // If CONFIG, second arg must be measurementId.
        await gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams, gtagParams);
      } else if (command === "consent" /* GtagCommand.CONSENT */) {
        // If CONFIG, second arg must be measurementId.
        gtagCore("consent" /* GtagCommand.CONSENT */, 'update', gtagParams);
      } else {
        // If SET, second arg must be params.
        gtagCore("set" /* GtagCommand.SET */, idOrNameOrParams);
      }
    } catch (e) {
      logger.error(e);
    }
  }

Not sure the Firebase gtagWrapper implements the get method. Or am I missing something here?

dwyfrequency commented 1 year ago

@abhi-gn we need to update the firebase gtag wrapper implementation to account for get calls. The wrapper is limited and doesn't take into account commands other than event, config, consent, and set https://github.com/firebase/firebase-js-sdk/blob/3ee35f9ae575018fa7617bd3ac69b99fbb5cc8e6/packages/analytics/src/helpers.ts#L260. For now, use the example https://github.com/firebase/firebase-js-sdk/issues/6881#issuecomment-1400895044 in a basic html project with no firebase installed.

Using a basic project with gtag, are you able to see if client_id will suffice? Also, would you mind sharing a screenshot or other resource of how you plan to track this in Google Analytics

abhi-gn commented 1 year ago

@dwyfrequency Thanks for clearing this up. We are already in the process of setting up Firebase analytics for our React web project and using Firebase JS SDK for that.

Now we want to track the tab close event with some collated data as attributes and were looking for ways to achieve that. One obvious way is to call an api using the browser's sendBeacon method. Which is where we are trying to call the /mp/collect api, kind of like your example but instead of a GA tag we have Firebase JS sdk.

It is still unclear to me whether Firebase sdk automatically handles sending of events using sendBeacon in cases like tab close etc. (Would be great if it does :) )

dwyfrequency commented 1 year ago

@abhi-gn would you mind raising a separate issue for this question and link it? The latest inquiry deviates from the original question raised by @twittergadungan.

dwyfrequency commented 1 year ago

I was able to use the client_id from gtag with measurement protocol. @twittergadungan are you able to do the same? I am not sure the best way to see whether it is considered the same user in the Google Analytics dashboard but can see both events coming through. Note, it appears that MP events take much longer to propagate then do the events from gtag.

// gtag call
window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}
gtag("js", new Date());
const measurementId = "INSERT-ID-HERE";
gtag("set", { currency: "USD" });

gtag("event", "purchase", {
        client_id: "511285180.1674077316",
        user_id: "USERID1",
        timestamp_micros: "1674083595919000",
        non_personalized_ads: false,
        events: [
          {
            name: "purchase",
            params: {
              items: [
                { item_id: "ITEM_1", item_name: "coffee mugs", quantity: 1 },
              ],
              affiliation: "GOOGLE STORE",
            },
          },
        ],
      });

gtag("get", measurementId, "client_id", (clientID) => {
  console.log({ clientID }); // gets you the client id for MP
}); 
// MP call
POST https://www.google-analytics.com/mp/collect?api_secret=<INSERT-SECRET-HERE>&measurement_id=<INSERT-ID-HERE>
HOST: www.google-analytics.com
Content-Type: application/json

{"client_id":"<ID-FROM-GTAG-GET>","user_id":"USERID1","timestamp_micros":"1674152143864000","non_personalized_ads":false,"events":[{"name":"refund","params":{"items":[{"item_id":"ITEM_1","item_name":"coffee mugs","quantity":1}],"affiliation":"GOOGLE STORE"}}]}

Screenshot 2023-01-23 at 2 51 49 PM

@twittergadungan you mentioned so unfortunately I can confirm using client_id as a replacement for app_instance_id does not work. Can you show a screenshot of how you verified this does not work in Google Analytics? We were able to call the MP endpoint in my linked example with client_id and we would like to see how you determined it was an unsuitable.

twittergadungan commented 1 year ago

@dwyfrequency

hi Sorry for late response. here is what I did:

  1. I try to get the client id using gtag on my Vue Web app, the client id was something like this: 204xxxxx07.1662xxxx54. here is the code to get the client id from gtag
function gtag(){
    dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'code-gtag-configxxxxxx');
gtag('get', 'code-gtag-configxxxxxx', 'client_id', (client_id) => {
    gtag_client_id= client_id
    console.log("client id: ", client_id)
})
  1. I make a request using postman to GA4 measurement protocol server using JSON body and query parameters like the image below. I made a purchase event which is a conversion event.
Screenshot 2023-02-23 at 15 13 13

as you can see, I use web firebase_app_id in the query parameter, the id is something like this 1:1******26811:web:4b*******bc7e13ae5f9e . and I got 204 status code that indicates that the request had succeeded.

  1. I checked if the data had been recorded correctly on Firebase console (realtime analytics tab) like this
Screenshot 2023-02-23 at 15 49 52

as you can see, the purchase event is not recorded in Firebase console

  1. but if I change the firebase_app_id from web ( 1:1******26811:web:4b*******bc7e13ae5f9e ) to Android firebase app id (1:121******811:android:674*******da4fae5f9e), I can see the purchase event is recorded on my Firebase console
dwyfrequency commented 1 year ago

@twittergadungan can you check again in 24 hours? Previously, I noticed , it appears that MP events take much longer to propagate then do the events from gtag. Gtag events were almost immediate but I usually saw Measurement Protocol events in GA the next day.

twittergadungan commented 1 year ago

@dwyfrequency

Unfortunately after 3 days, I still can't see the purchase events on GA4 dashboard.

Screenshot 2023-02-27 at 07 57 38
goodspeed3 commented 1 year ago

Works for me, thanks. @twittergadungan try sending measurementId instead of firebase_app_id

twittergadungan commented 1 year ago

@goodspeed3

so I just need to rename the firebase_app_id to be measurementId in the query parameters but the value is still the same?

dwyfrequency commented 1 year ago

@twittergadungan yes, it should be measurement_id.

image

In the event builder, you can see it mentions using gtag for web apps which is what the firebase js sdk uses under the hood. First, choose the client you are using with the toggle below. Mobile implementations should use firebase, and web implementations should use gtag.js

image

One thing to note is that the events from Measurement Protocol seem to take a long time to propagate into the GA dashboard. I'm going to keep investigating why this is the case, possibly my own settings. For example, I sent this from postman yesterday and it did eventually appear but it took a long time.

curl --location --request POST 'https://www.google-analytics.com/mp/collect?api_secret=SECRET&measurement_id=MEASUREMENT_ID' \
--header 'Content-Type: text/plain' \
--data-raw '{"client_id":"511285180.1674077316","user_id":"USERID1","timestamp_micros":"1677512966734000","non_personalized_ads":false,"events":[{"name":"purchase","params":{"items":[{"item_id":"ITEM_1","item_name":"coffee mugs","quantity":1}],"affiliation":"GOOGLE STORE"}}]}'

image

twittergadungan commented 1 year ago

@dwyfrequency

Thank you very much for the response, I really appreciate it. I will try and I will update the result

but to be honest I am worried if I create using gtag in the backend, the data will not appear and will not integrate into my Firebase console. I need to use Firebase because I assume I can easily integrate the analytics to other Firebase products like Firebase Remote config, Firebase A/B Testing, Firebase Cloud Messaging etc. so I can track user behaviour in all devices. Android, iOS and Web App

Thats why I create a feature request so I can use Measurement Protocol on my Firebase Web App

twittergadungan commented 1 year ago

@dwyfrequency

I can see the purchase event data when creating data via gtag, and I can immediately see the data on my Firebase Realtime Analytics , and you are right it needs time to appear on GA dashboard.

Screenshot 2023-03-01 at 09 57 50
dwyfrequency commented 1 year ago

@twittergadungan happy to see that's working for you. Would having an explicit method in the analytics package like getClientId() be helpful to your use case? If so, I'll submit an internal proposal to.

twittergadungan commented 1 year ago

@twittergadungan happy to see that's working for you. Would having an explicit method in the analytics package like getClientId() be helpful to your use case? If so, I'll submit an internal proposal to.

yes, I think it will be great and simplify the process to get the client ID without using gtag and measurement ID

rromanchuk commented 9 months ago

lol i was literally fishing it out of the cookies for S2S, so smelly.

  # Cookie where client_id is stored
  # _ga=GA1.3.xxxxxxx.xxxxxxxx
  def ga
    @ga ||= cookies[:_ga].presence
  end

  # "GS1.1.xxxxx.70.1.xxxxxxx.60.0.0"
  # _ga_MEASUREMENTID
  def ga_session
    @ga_session ||= cookies[Rails.configuration.general.firebase_session_cookie].presence
  end

  def cid
    @cid ||= ga&.split(".")&.last(2)&.join(".")
  end

  def sid
    @sid ||= ga_session&.split(".")&.[](2)
  end
hsubox76 commented 6 months ago

Oops, forgot to update this issue but this method was added in April 2023: https://firebase.google.com/support/release-notes/js#version_9210_-_april_27_2023