Convert to GA4 format

- It is recommended to use "lowercase + underscore format" for events to keep things neat, since the dashboard will be mixed with Automated and Recommended events.

- GA4 event structure is no longer the same as UA's, and the recommendation is to retructure rather than trying to mimic the old pattern.

- Always check the Recommended events to see if there is an equivalent, and use the exact name. GA4 might add automated features for these events in the future, and we'll benefit from it without code changes and invalidating existing data.

- pageView: use default snippet behavior instead of manually sending
Start converting to GA4...

- Outbound click are automatically handled.
This commit is contained in:
infinite-persistence 2021-10-16 21:13:35 +08:00
parent dab1ca1cb7
commit f6e60abbf5
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
5 changed files with 46 additions and 84 deletions

View file

@ -60,7 +60,6 @@
"match-sorter": "^6.3.0", "match-sorter": "^6.3.0",
"parse-duration": "^1.0.0", "parse-duration": "^1.0.0",
"react-datetime-picker": "^3.2.1", "react-datetime-picker": "^3.2.1",
"react-ga": "^3.3.0",
"react-plastic": "^1.1.1", "react-plastic": "^1.1.1",
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",
"remove-markdown": "^0.3.0", "remove-markdown": "^0.3.0",

View file

@ -1,6 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html dir="ltr"> <html dir="ltr">
<head> <head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BB8DNPB73F"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
});
gtag('js', new Date());
gtag('config', 'G-BB8DNPB73F');
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />

View file

@ -1,8 +1,6 @@
// @flow // @flow
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import ReactGA from 'react-ga';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { history } from './store';
import { SDK_API_PATH } from './index'; import { SDK_API_PATH } from './index';
// import getConnectionSpeed from 'util/detect-user-bandwidth'; // import getConnectionSpeed from 'util/detect-user-bandwidth';
@ -16,7 +14,6 @@ import { SDK_API_PATH } from './index';
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev');
const ODYSEE_UA_ID = 'UA-60403362-12';
export const SHARE_INTERNAL = 'shareInternal'; export const SHARE_INTERNAL = 'shareInternal';
@ -26,7 +23,6 @@ const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
type Analytics = { type Analytics = {
error: (string) => Promise<any>, error: (string) => Promise<any>,
sentryError: ({} | string, {}) => Promise<any>, sentryError: ({} | string, {}) => Promise<any>,
pageView: (string, ?string) => void,
setUser: (Object) => void, setUser: (Object) => void,
toggleInternal: (boolean, ?boolean) => void, toggleInternal: (boolean, ?boolean) => void,
apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>, apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>,
@ -248,7 +244,7 @@ const analytics: Analytics = {
bitrateAsBitsPerSecond = videoBitrate; bitrateAsBitsPerSecond = videoBitrate;
sendPromMetric('time_to_start', timeToStartVideo); sendPromMetric('time_to_start', timeToStartVideo);
sendGaEvent('Media', 'TimeToStart', claimId, timeToStartVideo); sendGaEvent('video_time_to_start', { claim_id: claimId, time: timeToStartVideo });
}, },
error: (message) => { error: (message) => {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -274,16 +270,9 @@ const analytics: Analytics = {
} }
}); });
}, },
pageView: (path, search) => {
if (internalAnalyticsEnabled) {
ReactGA.pageview(path);
}
},
setUser: (userId) => { setUser: (userId) => {
if (internalAnalyticsEnabled && userId) { if (internalAnalyticsEnabled && userId && window.gtag) {
ReactGA.set({ window.gtag('set', { user_id: userId });
userId,
});
} }
}, },
toggleInternal: (enabled: boolean): void => { toggleInternal: (enabled: boolean): void => {
@ -348,72 +337,57 @@ const analytics: Analytics = {
} }
}, },
adsFetchedEvent: () => { adsFetchedEvent: () => {
sendGaEvent('Media', 'AdsFetched'); sendGaEvent('ad_fetched');
}, },
adsReceivedEvent: (response) => { adsReceivedEvent: (response) => {
sendGaEvent('Media', 'AdsReceived', JSON.stringify(response)); sendGaEvent('ad_received', { response: JSON.stringify(response) });
}, },
adsErrorEvent: (response) => { adsErrorEvent: (response) => {
sendGaEvent('Media', 'AdsError', JSON.stringify(response)); sendGaEvent('ad_error', { response: JSON.stringify(response) });
}, },
playerLoadedEvent: (embedded) => { playerLoadedEvent: (embedded) => {
sendGaEvent('Player', 'Loaded', embedded ? 'embedded' : 'onsite'); sendGaEvent('player', { action: 'loaded', type: embedded ? 'embedded' : 'onsite' });
}, },
playerStartedEvent: (embedded) => { playerStartedEvent: (embedded) => {
sendGaEvent('Player', 'Started', embedded ? 'embedded' : 'onsite'); sendGaEvent('player', { action: 'started', type: embedded ? 'embedded' : 'onsite' });
}, },
tagFollowEvent: (tag, following) => { tagFollowEvent: (tag, following) => {
sendGaEvent('Tag', following ? 'Tag-Follow' : 'Tag-Unfollow', tag); sendGaEvent(following ? 'tag_follow' : 'tag_unfollow', { tag });
}, },
channelBlockEvent: (uri, blocked, location) => { channelBlockEvent: (uri, blocked, location) => {
sendGaEvent(blocked ? 'Channel-Hidden' : 'Channel-Unhidden', uri); sendGaEvent(blocked ? 'channel_hidden' : 'channel_unhidden', { uri });
}, },
emailProvidedEvent: () => { emailProvidedEvent: () => {
sendGaEvent('Engagement', 'Email-Provided'); sendGaEvent('engagement', { type: 'email_provided' });
}, },
emailVerifiedEvent: () => { emailVerifiedEvent: () => {
sendGaEvent('Engagement', 'Email-Verified'); sendGaEvent('engagement', { type: 'email_verified' });
}, },
rewardEligibleEvent: () => { rewardEligibleEvent: () => {
sendGaEvent('Engagement', 'Reward-Eligible'); sendGaEvent('engagement', { type: 'reward_eligible' });
}, },
openUrlEvent: (url: string) => { openUrlEvent: (url: string) => {
sendGaEvent('Engagement', 'Open-Url', url); sendGaEvent('engagement', { type: 'open_url', url });
}, },
trendingAlgorithmEvent: (trendingAlgorithm: string) => { trendingAlgorithmEvent: (trendingAlgorithm: string) => {
sendGaEvent('Engagement', 'Trending-Algorithm', trendingAlgorithm); sendGaEvent('engagement', { type: 'trending_algorithm', trending_algorithm: trendingAlgorithm });
}, },
startupEvent: () => { startupEvent: () => {
sendGaEvent('Startup', 'Startup'); // TODO: This can be removed (use the automated 'session_start' instead).
// sendGaEvent('startup', 'startup');
}, },
readyEvent: (timeToReady: number) => { readyEvent: (timeToReadyMs: number) => {
sendGaEvent('Startup', 'App-Ready'); sendGaEvent('startup_app_ready', { time_to_ready_ms: timeToReadyMs });
sendGaTimingEvent('Startup', 'App-Ready', timeToReady);
}, },
purchaseEvent: (purchaseInt: number) => { purchaseEvent: (purchaseInt: number) => {
sendGaEvent('Purchase', 'Purchase-Complete', undefined, purchaseInt); // https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase
sendGaEvent('purchase', { value: purchaseInt });
}, },
}; };
function sendGaEvent(category, action, label, value) { function sendGaEvent(event: string, params?: { [string]: string | number }) {
if (internalAnalyticsEnabled && isProduction) { if (internalAnalyticsEnabled && isProduction && window.gtag) {
ReactGA.event({ window.gtag('event', event, params);
category,
action,
...(label ? { label } : {}),
...(value ? { value } : {}),
});
}
}
function sendGaTimingEvent(category: string, action: string, timeInMs: number, label?: string) {
if (internalAnalyticsEnabled && isProduction) {
ReactGA.timing({
category,
variable: action,
value: timeInMs,
...(label ? { label } : {}),
});
} }
} }
@ -426,27 +400,12 @@ function sendPromMetric(name: string, value?: number) {
} }
} }
const gaTrackers = [{ trackingId: ODYSEE_UA_ID }]; // Activate
if (internalAnalyticsEnabled && isProduction && window.gtag) {
ReactGA.initialize(gaTrackers, { window.gtag('consent', 'update', {
testMode: process.env.NODE_ENV !== 'production', ad_storage: 'granted',
cookieDomain: 'auto', analytics_storage: 'granted',
siteSpeedSampleRate: 100, });
// un-comment to see events as they are sent to google }
// debug: true,
});
// Manually call the first page view
// React Router doesn't include this on `history.listen`
analytics.pageView(window.location.pathname + window.location.search, window.location.search);
// Listen for url changes and report
// This will include search queries
history.listen((location) => {
const { pathname, search } = location;
const page = `${pathname}${search}`;
analytics.pageView(page, search);
});
export default analytics; export default analytics;

View file

@ -5,7 +5,6 @@ import Icon from 'component/common/icon';
import classnames from 'classnames'; import classnames from 'classnames';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import { OutboundLink } from 'react-ga';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import useCombinedRefs from 'effects/use-combined-refs'; import useCombinedRefs from 'effects/use-combined-refs';
@ -147,9 +146,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
if (href || (navigate && navigate.startsWith('http'))) { if (href || (navigate && navigate.startsWith('http'))) {
// TODO: replace the below with an outbound link tracker for matomo // TODO: replace the below with an outbound link tracker for matomo
return ( return (
<OutboundLink <a
eventLabel="outboundClick"
to={href || navigate}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href={href || navigate} href={href || navigate}
@ -158,10 +155,9 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
onClick={onClick} onClick={onClick}
aria-label={ariaLabel} aria-label={ariaLabel}
disabled={disabled} // is there a reason this wasn't here before? disabled={disabled} // is there a reason this wasn't here before?
{...otherProps}
> >
{content} {content}
</OutboundLink> </a>
); );
} }

View file

@ -13480,11 +13480,6 @@ react-fit@^1.0.3:
detect-element-overflow "^1.2.0" detect-element-overflow "^1.2.0"
prop-types "^15.6.0" prop-types "^15.6.0"
react-ga@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca"
integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==
react-google-recaptcha@^2.0.1: react-google-recaptcha@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"