diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 93f172793..1c1673f4c 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -32,6 +32,8 @@ jobs: GA_TRACKING_ID: ${{ vars.GA_TRACKING_ID }} AD_CONVERSION_ID: ${{ vars.AD_CONVERSION_ID }} FACEBOOK_PIXEL_ID: ${{ vars.FACEBOOK_PIXEL_ID }} + GRE_API_KEY: ${{ vars.GRE_API_KEY }} + GRE_PROJECT_ID: ${{ vars.GRE_PROJECT_ID }} STRIPE_PUBLIC_KEY: ${{ vars.STRIPE_PUBLIC_KEY }} CRISP_WEBSITE_ID: ${{ vars.CRISP_WEBSITE_ID }} SENTRY_DSN: ${{ vars.SENTRY_DSN }} diff --git a/src/nuxt.config.js b/src/nuxt.config.js index 22bca1a1b..7e4ad40de 100644 --- a/src/nuxt.config.js +++ b/src/nuxt.config.js @@ -12,6 +12,8 @@ const { STRIPE_PUBLIC_KEY, GA_TRACKING_ID, AD_CONVERSION_ID, + GRE_API_KEY, + GRE_PROJECT_ID, EXTERNAL_URL, FACEBOOK_PIXEL_ID, } = process.env; @@ -25,6 +27,8 @@ const nuxtConfig = { GA_TRACKING_ID, AD_CONVERSION_ID, FACEBOOK_PIXEL_ID, + GRE_API_KEY, + GRE_PROJECT_ID, SITE_NAME, EXTERNAL_URL, }, @@ -234,6 +238,7 @@ const nuxtConfig = { "'wasm-unsafe-eval'", '*.google-analytics.com', 'www.googletagmanager.com', + 'www.gstatic.com', 'www.google.com', 'googleads.g.doubleclick.net', 'www.googleadservices.com', @@ -312,6 +317,7 @@ const nuxtConfig = { '~/plugins/axios.js', '~/plugins/likecoin-ui-vue.js', '~/plugins/portal-vue.js', + { src: '~/plugins/gre.client.js', mode: 'client' }, { src: '~/plugins/gtag.client.js', mode: 'client' }, { src: '~/plugins/ui-plugin.client.js', ssr: false }, { src: '~/plugins/vue-cookie.client.js', ssr: false }, diff --git a/src/pages/index.vue b/src/pages/index.vue index 5a468ddd5..cc36f398b 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -756,7 +756,7 @@ import { Swiper, SwiperSlide } from 'vue-awesome-swiper'; import bookstoreMixin from '~/mixins/bookstore'; -import { logTrackerEvent } from '~/util/EventLogger'; +import { logTrackerEvent, logRetailEvent } from '~/util/EventLogger'; import { fisherShuffle } from '~/util/misc'; const SIGNATURE_BANNER_NAMES = [ @@ -944,6 +944,7 @@ export default { }, }, mounted() { + logRetailEvent(this, 'home-page-view'); window.addEventListener('scroll', this.handleScroll); }, beforeDestroy() { diff --git a/src/plugins/gre.client.js b/src/plugins/gre.client.js new file mode 100644 index 000000000..d75887bbb --- /dev/null +++ b/src/plugins/gre.client.js @@ -0,0 +1,74 @@ +/* eslint-disable no-underscore-dangle */ + +class GoogleRetailPixel { + userId; + visitorId; + apiKey; + projectId; + + constructor(apiKey, projectId) { + this.apiKey = apiKey; + this.projectId = projectId; + window._gre = window._gre || []; + window._gre.push(['apiKey', apiKey]); + window._gre.push(['projectId', projectId]); + window._gre.push(['locationId', 'global']); + window._gre.push(['catalogId', 'default_catalog']); + } + + setUserId(userId) { + this.userId = userId; + } + + setVisitorId(visitorId) { + this.visitorId = visitorId; + } + + logEvent(eventType, payload = {}, { attributionToken, experimentIds } = {}) { + if (!this.visitorId) { + return; + } + const event = { + eventType, + attributionToken, + experimentIds, + visitorId: this.visitorId, + userInfo: this.userId + ? { + userId: this.userId, + } + : undefined, + ...payload, + }; + // HACK: cloud_retail does not replace _gre on init + // cloud_retail only calls logEvent once on _gre and + // it does not even clear _gre after that + if (window.cloud_retail) { + window.cloud_retail.logEvent([ + ['apiKey', this.apiKey], + ['projectId', this.projectId], + ['locationId', 'global'], + ['catalogId', 'default_catalog'], + ['logEvent', event], + ]); + } else { + window._gre.push(['logEvent', event]); + } + } +} + +export default (ctx, inject) => { + if (process.env.GRE_API_KEY && process.env.GRE_PROJECT_ID) { + const gre = new GoogleRetailPixel( + process.env.GRE_API_KEY, + process.env.GRE_PROJECT_ID + ); + const d = document; + const s = d.createElement('script'); + s.src = 'https://www.gstatic.com/retail/v2_event.js'; + s.async = 1; + d.getElementsByTagName('head')[0].appendChild(s); + + inject('gre', gre); + } +}; diff --git a/src/store/modules/actions/user.js b/src/store/modules/actions/user.js index 73ebcc780..42b6ff218 100644 --- a/src/store/modules/actions/user.js +++ b/src/store/modules/actions/user.js @@ -49,6 +49,9 @@ export function setUserCivicLiker({ commit }, { civicLikerVersion = 1 } = {}) { export function setGaClientId({ commit }, gaClientId) { commit(types.USER_SET_GA_CLIENT_ID, gaClientId); + if (this.$gre) { + this.$gre.setVisitorId(gaClientId); + } } export function setGaSessionId({ commit }, gaSessionId) { diff --git a/src/util/EventLogger.js b/src/util/EventLogger.js index 2d64832fa..e96eeb007 100644 --- a/src/util/EventLogger.js +++ b/src/util/EventLogger.js @@ -30,6 +30,9 @@ export function resetLoggerUser(vue) { if (vue.$sentry) { vue.$sentry.setUser({}); } + if (vue.$gre) { + vue.$gre.setUserId(null); + } if (vue.$gtag) { vue.$gtag.set({ userId: null }); vue.$gtag.set({ user_id: null }); @@ -54,6 +57,11 @@ export async function setLoggerUser( vue.$sentry.setUser(opt); } try { + let hashedId = await digestMessage(wallet); + hashedId = hexString(hashedId); + if (vue.$gre) { + vue.$gre.setUserId(hashedId); + } if (vue.$gtag) { if (event === 'signup') { vue.$gtag.event('sign_up', { method }); @@ -61,20 +69,17 @@ export async function setLoggerUser( vue.$gtag.event('login', { method }); } } - if (!hasDoNotTrack()) { - if (vue.$gtag) { - const hashedId = await getHashedUserId(wallet); - vue.$gtag.set({ userId: hashedId }); - // HACK: use .set to mitigate connected site user_id issue - // https://support.google.com/analytics/answer/9973999?hl=en - // vue.$gtag.config({ user_id: hashedId }); - vue.$gtag.set({ user_id: hashedId }); - } - if (vue.$fb && FACEBOOK_PIXEL_ID) { - vue.$fb.init(FACEBOOK_PIXEL_ID, { - external_id: wallet, - }); - } + if (vue.$gtag) { + vue.$gtag.set({ userId: hashedId }); + // HACK: use .set to mitigate connected site user_id issue + // https://support.google.com/analytics/answer/9973999?hl=en + // vue.$gtag.config({ user_id: hashedId }); + vue.$gtag.set({ user_id: hashedId }); + } + if (vue.$fb && FACEBOOK_PIXEL_ID) { + vue.$fb.init(FACEBOOK_PIXEL_ID, { + external_id: wallet, + }); } if (vue.$crisp) { vue.$crisp.push(['set', 'session:data', [[['like_wallet', wallet]]]]); @@ -154,6 +159,35 @@ export function logTrackerEvent( } } +export function logRetailEvent(vue, eventType, payload) { + try { + if (vue.$gre) { + if (!vue.$gre.visitorId) { + // HACK: query in gtag if no visitor Id + // multiple concurrent queries might occur + // if logRetailEvent is called multiple times + // but all should yield same result anyway + if (vue.$gtag && process.env.GA_TRACKING_ID) { + vue.$gtag.query( + 'get', + process.env.GA_TRACKING_ID, + 'client_id', + id => { + vue.$gre.setVisitorId(id); + vue.$gre.logEvent(eventType, payload); + } + ); + } + } else { + vue.$gre.logEvent(eventType, payload); + } + } + } catch (err) { + console.error('logging error:'); // eslint-disable-line no-console + console.error(err); // eslint-disable-line no-console + } +} + export function logPurchaseFlowEvent( vue, event,