Additional GA events via redux/lbryio hook
## Issue 85 Add additional GA events ## Approach Instead of placing analytic calls all over the GUI code (no separation of concerns), try to do it through a redux middleware instead. ## Changes - Updated GA event and parameter naming after understanding how reporting works. - Removed unused analytics.
This commit is contained in:
parent
e14ec9b83e
commit
b7685a151d
8 changed files with 219 additions and 47 deletions
|
@ -1,6 +1,7 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
|
import analytics from 'analytics';
|
||||||
|
|
||||||
const Lbryio = {
|
const Lbryio = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -81,7 +82,10 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
|
||||||
url = `${Lbryio.CONNECTION_STRING}${resource}/${action}`;
|
url = `${Lbryio.CONNECTION_STRING}${resource}/${action}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeRequest(url, options).then((response) => response.data);
|
return makeRequest(url, options).then((response) => {
|
||||||
|
sendCallAnalytics(resource, action, params);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -233,4 +237,23 @@ Lbryio.setOverride = (methodName, newMethod) => {
|
||||||
Lbryio.overrides[methodName] = newMethod;
|
Lbryio.overrides[methodName] = newMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function sendCallAnalytics(resource, action, params) {
|
||||||
|
switch (resource) {
|
||||||
|
case 'customer':
|
||||||
|
if (action === 'tip') {
|
||||||
|
analytics.reportEvent('spend_virtual_currency', {
|
||||||
|
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#spend_virtual_currency
|
||||||
|
value: params.amount,
|
||||||
|
virtual_currency_name: params.currency.toLowerCase(),
|
||||||
|
item_name: 'tip',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Lbryio;
|
export default Lbryio;
|
||||||
|
|
131
ui/analytics.js
131
ui/analytics.js
|
@ -1,7 +1,36 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import { SDK_API_PATH } from './index';
|
import { SDK_API_PATH } from './index';
|
||||||
|
|
||||||
|
// --- GA ---
|
||||||
|
// - Events: 500 max (cannot be deleted).
|
||||||
|
// - Dimensions: 25 max (cannot be deleted, but can be "archived"). Usually
|
||||||
|
// tied to an event parameter for reporting purposes.
|
||||||
|
//
|
||||||
|
// Given the limitations above, we need to plan ahead before adding new Events
|
||||||
|
// and Parameters.
|
||||||
|
//
|
||||||
|
// Events:
|
||||||
|
// - Find a Recommended Event that is closest to what you need.
|
||||||
|
// https://support.google.com/analytics/answer/9267735?hl=en
|
||||||
|
// - If doesn't exist, use a Custom Event.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - Custom parameters don't appear in automated reports until they are tied to
|
||||||
|
// a Dimension.
|
||||||
|
// - Add your entry to GA_DIMENSIONS below -- tt allows us to keep track so that
|
||||||
|
// we don't exceed the limit. Re-use existing parameters if possible.
|
||||||
|
// - Register the Dimension in GA Console to make it visible in reports.
|
||||||
|
|
||||||
|
export const GA_DIMENSIONS = {
|
||||||
|
TYPE: 'type',
|
||||||
|
ACTION: 'action',
|
||||||
|
VALUE: 'value',
|
||||||
|
DURATION_MS: 'duration_ms',
|
||||||
|
};
|
||||||
|
|
||||||
// import getConnectionSpeed from 'util/detect-user-bandwidth';
|
// import getConnectionSpeed from 'util/detect-user-bandwidth';
|
||||||
|
|
||||||
// let userDownloadBandwidthInBitsPerSecond;
|
// let userDownloadBandwidthInBitsPerSecond;
|
||||||
|
@ -29,8 +58,8 @@ type Analytics = {
|
||||||
apiLogPublish: (ChannelClaim | StreamClaim) => void,
|
apiLogPublish: (ChannelClaim | StreamClaim) => void,
|
||||||
apiSyncTags: ({}) => void,
|
apiSyncTags: ({}) => void,
|
||||||
tagFollowEvent: (string, boolean, ?string) => void,
|
tagFollowEvent: (string, boolean, ?string) => void,
|
||||||
playerLoadedEvent: (?boolean) => void,
|
playerLoadedEvent: (string, ?boolean) => void,
|
||||||
playerStartedEvent: (?boolean) => void,
|
playerVideoStartedEvent: (?boolean) => void,
|
||||||
videoStartEvent: (string, number, string, number, string, any, number) => void,
|
videoStartEvent: (string, number, string, number, string, any, number) => void,
|
||||||
videoIsPlaying: (boolean, any) => void,
|
videoIsPlaying: (boolean, any) => void,
|
||||||
videoBufferEvent: (
|
videoBufferEvent: (
|
||||||
|
@ -46,8 +75,6 @@ type Analytics = {
|
||||||
}
|
}
|
||||||
) => Promise<any>,
|
) => Promise<any>,
|
||||||
adsFetchedEvent: () => void,
|
adsFetchedEvent: () => void,
|
||||||
adsReceivedEvent: (any) => void,
|
|
||||||
adsErrorEvent: (any) => void,
|
|
||||||
emailProvidedEvent: () => void,
|
emailProvidedEvent: () => void,
|
||||||
emailVerifiedEvent: () => void,
|
emailVerifiedEvent: () => void,
|
||||||
rewardEligibleEvent: () => void,
|
rewardEligibleEvent: () => void,
|
||||||
|
@ -55,6 +82,7 @@ type Analytics = {
|
||||||
purchaseEvent: (number) => void,
|
purchaseEvent: (number) => void,
|
||||||
readyEvent: (number) => void,
|
readyEvent: (number) => void,
|
||||||
openUrlEvent: (string) => void,
|
openUrlEvent: (string) => void,
|
||||||
|
reportEvent: (string, any) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogPublishParams = {
|
type LogPublishParams = {
|
||||||
|
@ -67,6 +95,8 @@ type LogPublishParams = {
|
||||||
let internalAnalyticsEnabled: boolean = IS_WEB || false;
|
let internalAnalyticsEnabled: boolean = IS_WEB || false;
|
||||||
// let thirdPartyAnalyticsEnabled: boolean = IS_WEB || false;
|
// let thirdPartyAnalyticsEnabled: boolean = IS_WEB || false;
|
||||||
|
|
||||||
|
const isGaAllowed = internalAnalyticsEnabled && isProduction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the mobile device type viewing the data
|
* Determine the mobile device type viewing the data
|
||||||
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
||||||
|
@ -186,12 +216,8 @@ async function sendWatchmanData(body) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
console.log('ERROR FROM WATCHMAN BACKEND');
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
|
@ -244,7 +270,6 @@ const analytics: Analytics = {
|
||||||
bitrateAsBitsPerSecond = videoBitrate;
|
bitrateAsBitsPerSecond = videoBitrate;
|
||||||
|
|
||||||
sendPromMetric('time_to_start', timeToStartVideo);
|
sendPromMetric('time_to_start', timeToStartVideo);
|
||||||
sendGaEvent('video_time_to_start', { claim_id: claimId, time: timeToStartVideo });
|
|
||||||
},
|
},
|
||||||
error: (message) => {
|
error: (message) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -271,7 +296,7 @@ const analytics: Analytics = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setUser: (userId) => {
|
setUser: (userId) => {
|
||||||
if (internalAnalyticsEnabled && userId && window.gtag) {
|
if (isGaAllowed && userId && window.gtag) {
|
||||||
window.gtag('set', { user_id: userId });
|
window.gtag('set', { user_id: userId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -339,54 +364,88 @@ const analytics: Analytics = {
|
||||||
adsFetchedEvent: () => {
|
adsFetchedEvent: () => {
|
||||||
sendGaEvent('ad_fetched');
|
sendGaEvent('ad_fetched');
|
||||||
},
|
},
|
||||||
adsReceivedEvent: (response) => {
|
playerLoadedEvent: (renderMode, embedded) => {
|
||||||
sendGaEvent('ad_received', { response: JSON.stringify(response) });
|
const RENDER_MODE_TO_EVENT = (renderMode) => {
|
||||||
|
switch (renderMode) {
|
||||||
|
case RENDER_MODES.VIDEO:
|
||||||
|
return 'loaded_video';
|
||||||
|
case RENDER_MODES.AUDIO:
|
||||||
|
return 'loaded_audio';
|
||||||
|
case RENDER_MODES.MARKDOWN:
|
||||||
|
return 'loaded_markdown';
|
||||||
|
case RENDER_MODES.IMAGE:
|
||||||
|
return 'loaded_image';
|
||||||
|
default:
|
||||||
|
return 'loaded_misc';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendGaEvent('player', {
|
||||||
|
[GA_DIMENSIONS.ACTION]: RENDER_MODE_TO_EVENT(renderMode),
|
||||||
|
[GA_DIMENSIONS.TYPE]: embedded ? 'embedded' : 'onsite',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
adsErrorEvent: (response) => {
|
playerVideoStartedEvent: (embedded) => {
|
||||||
sendGaEvent('ad_error', { response: JSON.stringify(response) });
|
sendGaEvent('player', {
|
||||||
},
|
[GA_DIMENSIONS.ACTION]: 'started_video',
|
||||||
playerLoadedEvent: (embedded) => {
|
[GA_DIMENSIONS.TYPE]: embedded ? 'embedded' : 'onsite',
|
||||||
sendGaEvent('player', { action: 'loaded', type: embedded ? 'embedded' : 'onsite' });
|
});
|
||||||
},
|
|
||||||
playerStartedEvent: (embedded) => {
|
|
||||||
sendGaEvent('player', { action: 'started', type: embedded ? 'embedded' : 'onsite' });
|
|
||||||
},
|
},
|
||||||
tagFollowEvent: (tag, following) => {
|
tagFollowEvent: (tag, following) => {
|
||||||
sendGaEvent(following ? 'tag_follow' : 'tag_unfollow', { tag });
|
sendGaEvent('tags', {
|
||||||
},
|
[GA_DIMENSIONS.ACTION]: following ? 'follow' : 'unfollow',
|
||||||
channelBlockEvent: (uri, blocked, location) => {
|
[GA_DIMENSIONS.VALUE]: tag,
|
||||||
sendGaEvent(blocked ? 'channel_hidden' : 'channel_unhidden', { uri });
|
});
|
||||||
},
|
},
|
||||||
emailProvidedEvent: () => {
|
emailProvidedEvent: () => {
|
||||||
sendGaEvent('engagement', { type: 'email_provided' });
|
sendGaEvent('engagement', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'email_provided',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
emailVerifiedEvent: () => {
|
emailVerifiedEvent: () => {
|
||||||
sendGaEvent('engagement', { type: 'email_verified' });
|
sendGaEvent('engagement', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'email_verified',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
rewardEligibleEvent: () => {
|
rewardEligibleEvent: () => {
|
||||||
sendGaEvent('engagement', { type: 'reward_eligible' });
|
sendGaEvent('engagement', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'reward_eligible',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
openUrlEvent: (url: string) => {
|
openUrlEvent: (url: string) => {
|
||||||
sendGaEvent('engagement', { type: 'open_url', url });
|
sendGaEvent('engagement', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'open_url',
|
||||||
|
url,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
trendingAlgorithmEvent: (trendingAlgorithm: string) => {
|
trendingAlgorithmEvent: (trendingAlgorithm: string) => {
|
||||||
sendGaEvent('engagement', { type: 'trending_algorithm', trending_algorithm: trendingAlgorithm });
|
sendGaEvent('engagement', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'trending_algorithm',
|
||||||
|
trending_algorithm: trendingAlgorithm,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
startupEvent: () => {
|
startupEvent: () => {
|
||||||
// TODO: This can be removed (use the automated 'session_start' instead).
|
// TODO: This can be removed (use the automated 'session_start' instead).
|
||||||
// sendGaEvent('startup', 'startup');
|
// sendGaEvent('app_diagnostics', 'startup');
|
||||||
},
|
},
|
||||||
readyEvent: (timeToReadyMs: number) => {
|
readyEvent: (timeToReadyMs: number) => {
|
||||||
sendGaEvent('startup_app_ready', { time_to_ready_ms: timeToReadyMs });
|
sendGaEvent('diag_app_ready', {
|
||||||
|
[GA_DIMENSIONS.DURATION_MS]: timeToReadyMs,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
purchaseEvent: (purchaseInt: number) => {
|
purchaseEvent: (purchaseInt: number) => {
|
||||||
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase
|
sendGaEvent('purchase', {
|
||||||
sendGaEvent('purchase', { value: purchaseInt });
|
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase
|
||||||
|
[GA_DIMENSIONS.VALUE]: purchaseInt,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reportEvent: (event: string, params?: { [string]: string | number }) => {
|
||||||
|
sendGaEvent(event, params);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendGaEvent(event: string, params?: { [string]: string | number }) {
|
function sendGaEvent(event: string, params?: { [string]: string | number }) {
|
||||||
if (internalAnalyticsEnabled && isProduction && window.gtag) {
|
if (isGaAllowed && window.gtag) {
|
||||||
window.gtag('event', event, params);
|
window.gtag('event', event, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -401,7 +460,7 @@ function sendPromMetric(name: string, value?: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate
|
// Activate
|
||||||
if (internalAnalyticsEnabled && isProduction && window.gtag) {
|
if (isGaAllowed && window.gtag) {
|
||||||
window.gtag('consent', 'update', {
|
window.gtag('consent', 'update', {
|
||||||
ad_storage: 'granted',
|
ad_storage: 'granted',
|
||||||
analytics_storage: 'granted',
|
analytics_storage: 'granted',
|
||||||
|
|
|
@ -48,9 +48,9 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { embedded } = this.props;
|
const { renderMode, embedded } = this.props;
|
||||||
window.addEventListener('keydown', this.escapeListener, true);
|
window.addEventListener('keydown', this.escapeListener, true);
|
||||||
analytics.playerLoadedEvent(embedded);
|
analytics.playerLoadedEvent(renderMode, embedded);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -60,9 +60,7 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
escapeListener(e: SyntheticKeyboardEvent<*>) {
|
escapeListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
if (e.keyCode === KEYCODES.ESCAPE) {
|
if (e.keyCode === KEYCODES.ESCAPE) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.exitFullscreen();
|
this.exitFullscreen();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,8 +179,7 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send matomo event (embedded is boolean)
|
analytics.playerVideoStartedEvent(embedded);
|
||||||
analytics.playerStartedEvent(embedded);
|
|
||||||
|
|
||||||
// convert bytes to bits, and then divide by seconds
|
// convert bytes to bits, and then divide by seconds
|
||||||
const contentInBits = Number(claim.value.source.size) * 8;
|
const contentInBits = Number(claim.value.source.size) * 8;
|
||||||
|
@ -189,12 +188,21 @@ function VideoViewer(props: Props) {
|
||||||
if (durationInSeconds) {
|
if (durationInSeconds) {
|
||||||
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
|
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
|
||||||
}
|
}
|
||||||
// figure out what server the video is served from and then run start analytic event
|
|
||||||
|
// figure out what server the video is served from and then run start analytic event
|
||||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||||
// server string such as 'eu-p6'
|
// server string such as 'eu-p6'
|
||||||
let playerPoweredBy = response.headers.get('x-powered-by') || '';
|
let playerPoweredBy = response.headers.get('x-powered-by') || '';
|
||||||
// populates data for watchman, sends prom and matomo event
|
// populates data for watchman, sends prom and matomo event
|
||||||
analytics.videoStartEvent(claimId, timeToStartVideo, playerPoweredBy, userId, claim.canonical_url, this, bitrateAsBitsPerSecond);
|
analytics.videoStartEvent(
|
||||||
|
claimId,
|
||||||
|
timeToStartVideo,
|
||||||
|
playerPoweredBy,
|
||||||
|
userId,
|
||||||
|
claim.canonical_url,
|
||||||
|
this,
|
||||||
|
bitrateAsBitsPerSecond
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// hit backend to mark a view
|
// hit backend to mark a view
|
||||||
|
|
|
@ -24,6 +24,28 @@ import Lbry from 'lbry';
|
||||||
// import LbryFirst from 'extras/lbry-first/lbry-first';
|
// import LbryFirst from 'extras/lbry-first/lbry-first';
|
||||||
import { isClaimNsfw } from 'util/claim';
|
import { isClaimNsfw } from 'util/claim';
|
||||||
|
|
||||||
|
function resolveClaimTypeForAnalytics(claim) {
|
||||||
|
if (!claim) {
|
||||||
|
return 'undefined_claim';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (claim.value_type) {
|
||||||
|
case 'stream':
|
||||||
|
if (claim.value) {
|
||||||
|
if (!claim.value.source) {
|
||||||
|
return 'livestream';
|
||||||
|
} else {
|
||||||
|
return claim.value.stream_type;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'stream';
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// collection, channel, repost, undefined
|
||||||
|
return claim.value_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const NO_FILE = '---';
|
export const NO_FILE = '---';
|
||||||
export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
const publishPreview = (previewResponse) => {
|
const publishPreview = (previewResponse) => {
|
||||||
|
@ -56,6 +78,9 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
type: ACTIONS.PUBLISH_SUCCESS,
|
type: ACTIONS.PUBLISH_SUCCESS,
|
||||||
|
data: {
|
||||||
|
type: resolveClaimTypeForAnalytics(pendingClaim),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// We have to fake a temp claim until the new pending one is returned by claim_list_mine
|
// We have to fake a temp claim until the new pending one is returned by claim_list_mine
|
||||||
|
|
|
@ -347,9 +347,9 @@ export function doSendTip(params, isSupport, successCallback, errorCallback, sho
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const balance = selectBalance(state);
|
const balance = selectBalance(state);
|
||||||
const myClaims = selectMyClaimsRaw(state);
|
const myClaims = selectMyClaimsRaw(state);
|
||||||
|
const supportOwnClaim = myClaims ? myClaims.find((claim) => claim.claim_id === params.claim_id) : false;
|
||||||
|
|
||||||
const shouldSupport =
|
const shouldSupport = isSupport || supportOwnClaim;
|
||||||
isSupport || (myClaims ? myClaims.find((claim) => claim.claim_id === params.claim_id) : false);
|
|
||||||
|
|
||||||
if (balance - params.amount <= 0) {
|
if (balance - params.amount <= 0) {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -376,6 +376,10 @@ export function doSendTip(params, isSupport, successCallback, errorCallback, sho
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SUPPORT_TRANSACTION_COMPLETED,
|
type: ACTIONS.SUPPORT_TRANSACTION_COMPLETED,
|
||||||
|
data: {
|
||||||
|
amount: params.amount,
|
||||||
|
type: shouldSupport ? (supportOwnClaim ? 'support_own' : 'support_others') : 'tip',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (successCallback) {
|
if (successCallback) {
|
||||||
|
@ -395,6 +399,7 @@ export function doSendTip(params, isSupport, successCallback, errorCallback, sho
|
||||||
type: ACTIONS.SUPPORT_TRANSACTION_FAILED,
|
type: ACTIONS.SUPPORT_TRANSACTION_FAILED,
|
||||||
data: {
|
data: {
|
||||||
error: err,
|
error: err,
|
||||||
|
type: shouldSupport ? (supportOwnClaim ? 'support_own' : 'support_others') : 'tip',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
49
ui/redux/middleware/analytics.js
Normal file
49
ui/redux/middleware/analytics.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
import analytics, { GA_DIMENSIONS } from 'analytics';
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
export function createAnalyticsMiddleware() {
|
||||||
|
return (/* { dispatch, getState } */) => (next: any) => (action: { type: string, data: any }) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ACTIONS.SUPPORT_TRANSACTION_COMPLETED:
|
||||||
|
const { amount, type } = action.data;
|
||||||
|
analytics.reportEvent('spend_virtual_currency', {
|
||||||
|
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#spend_virtual_currency
|
||||||
|
value: amount,
|
||||||
|
virtual_currency_name: 'lbc',
|
||||||
|
item_name: type,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTIONS.COMMENT_CREATE_COMPLETED:
|
||||||
|
analytics.reportEvent('comments', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'create',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTIONS.COMMENT_CREATE_FAILED:
|
||||||
|
analytics.reportEvent('comments', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'create_fail',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTIONS.PUBLISH_SUCCESS:
|
||||||
|
analytics.reportEvent('publish', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'publish_success',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTIONS.PUBLISH_FAIL:
|
||||||
|
analytics.reportEvent('publish', {
|
||||||
|
[GA_DIMENSIONS.TYPE]: 'publish_fail',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { createMemoryHistory, createBrowserHistory } from 'history';
|
||||||
import { routerMiddleware } from 'connected-react-router';
|
import { routerMiddleware } from 'connected-react-router';
|
||||||
import createRootReducer from './reducers';
|
import createRootReducer from './reducers';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
|
import { createAnalyticsMiddleware } from 'redux/middleware/analytics';
|
||||||
import { buildSharedStateMiddleware } from 'redux/middleware/shared-state';
|
import { buildSharedStateMiddleware } from 'redux/middleware/shared-state';
|
||||||
import { doSyncLoop } from 'redux/actions/sync';
|
import { doSyncLoop } from 'redux/actions/sync';
|
||||||
import { getAuthToken } from 'util/saved-passwords';
|
import { getAuthToken } from 'util/saved-passwords';
|
||||||
|
@ -206,6 +207,8 @@ const sharedStateMiddleware = buildSharedStateMiddleware(triggerSharedStateActio
|
||||||
const rootReducer = createRootReducer(history);
|
const rootReducer = createRootReducer(history);
|
||||||
const persistedReducer = persistReducer(persistOptions, rootReducer);
|
const persistedReducer = persistReducer(persistOptions, rootReducer);
|
||||||
const bulkThunk = createBulkThunkMiddleware();
|
const bulkThunk = createBulkThunkMiddleware();
|
||||||
|
const analyticsMiddleware = createAnalyticsMiddleware();
|
||||||
|
|
||||||
const middleware = [
|
const middleware = [
|
||||||
sharedStateMiddleware,
|
sharedStateMiddleware,
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
|
@ -214,7 +217,9 @@ const middleware = [
|
||||||
routerMiddleware(history),
|
routerMiddleware(history),
|
||||||
thunk,
|
thunk,
|
||||||
bulkThunk,
|
bulkThunk,
|
||||||
|
analyticsMiddleware,
|
||||||
];
|
];
|
||||||
|
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
enableBatching(persistedReducer),
|
enableBatching(persistedReducer),
|
||||||
|
|
Loading…
Reference in a new issue