Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Rival
80a167e300 Add prices and currency to store analytics
Don't show in changelog
2025-09-16 12:12:47 +02:00
13 changed files with 284 additions and 104 deletions

View File

@@ -268,8 +268,9 @@ const BundlePageHeader = ({
bundleId: bundle.id,
bundleName: bundle.name,
bundleTag: bundle.tag,
currency: price ? price.currency : undefined,
usageType: 'default',
priceValue: price && price.value,
priceCurrency: price && price.currency,
});
setPurchasingBundleListingData(bundleListingData);

View File

@@ -157,10 +157,15 @@ export const BundleStoreStateProvider = ({
});
if (bundleListingData) {
const priceForUsageType = bundleListingData.prices.find(
price => price.usageType === 'default'
);
sendBundleInformationOpened({
bundleName: bundleListingData.name,
bundleId: bundleListingData.id,
source: 'web-link',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
shopNavigationState.openBundleInformationPage({
bundleListingData,

View File

@@ -186,10 +186,15 @@ const ExampleStore = ({
)
: [],
onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => {
const priceForUsageType = privateGameTemplateListingData.prices.find(
price => price.usageType === 'default'
);
sendGameTemplateInformationOpened({
gameTemplateName: privateGameTemplateListingData.name,
gameTemplateId: privateGameTemplateListingData.id,
source: 'examples-list',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
onSelectPrivateGameTemplateListingData(
privateGameTemplateListingData

View File

@@ -455,7 +455,8 @@ const PrivateAssetPackInformationPage = ({
assetPackTag: assetPack.tag,
assetPackKind: 'private',
usageType: selectedUsageType,
currency: price ? price.currency : undefined,
priceValue: price && price.value,
priceCurrency: price && price.currency,
});
setPurchasingPrivateAssetPackListingData(privateAssetPackListingData);
@@ -487,15 +488,6 @@ const PrivateAssetPackInformationPage = ({
return;
}
sendAssetPackBuyClicked({
assetPackId: assetPack.id,
assetPackName: assetPack.name,
assetPackTag: assetPack.tag,
assetPackKind: 'private',
currency: 'CREDITS',
usageType: selectedUsageType,
});
const currentCreditsAmount = limits.credits.userBalance.amount;
const assetPackPriceForUsageType = privateAssetPackListingData.creditPrices.find(
price => price.usageType === selectedUsageType
@@ -508,6 +500,17 @@ const PrivateAssetPackInformationPage = ({
return;
}
const assetPackCreditsAmount = assetPackPriceForUsageType.amount;
sendAssetPackBuyClicked({
assetPackId: assetPack.id,
assetPackName: assetPack.name,
assetPackTag: assetPack.tag,
assetPackKind: 'private',
priceValue: assetPackCreditsAmount,
priceCurrency: 'CREDITS',
usageType: selectedUsageType,
});
if (currentCreditsAmount < assetPackCreditsAmount) {
openCreditsPackageDialog({
missingCredits: assetPackCreditsAmount - currentCreditsAmount,

View File

@@ -355,8 +355,9 @@ const PrivateGameTemplateInformationPage = ({
gameTemplateId: gameTemplate.id,
gameTemplateName: gameTemplate.name,
gameTemplateTag: gameTemplate.tag,
currency: price ? price.currency : undefined,
usageType: selectedUsageType,
priceValue: price && price.value,
priceCurrency: price && price.currency,
});
setPurchasingPrivateGameTemplateListingData(
@@ -392,14 +393,6 @@ const PrivateGameTemplateInformationPage = ({
return;
}
sendGameTemplateBuyClicked({
gameTemplateId: gameTemplate.id,
gameTemplateName: gameTemplate.name,
gameTemplateTag: gameTemplate.tag,
usageType: selectedUsageType,
currency: 'CREDITS',
});
const currentCreditsAmount = limits.credits.userBalance.amount;
const gameTemplatePriceForUsageType = privateGameTemplateListingData.creditPrices.find(
price => price.usageType === selectedUsageType
@@ -412,6 +405,16 @@ const PrivateGameTemplateInformationPage = ({
return;
}
const gameTemplateCreditsAmount = gameTemplatePriceForUsageType.amount;
sendGameTemplateBuyClicked({
gameTemplateId: gameTemplate.id,
gameTemplateName: gameTemplate.name,
gameTemplateTag: gameTemplate.tag,
usageType: selectedUsageType,
priceValue: gameTemplateCreditsAmount,
priceCurrency: 'CREDITS',
});
if (currentCreditsAmount < gameTemplateCreditsAmount) {
openCreditsPackageDialog({
missingCredits: gameTemplateCreditsAmount - currentCreditsAmount,

View File

@@ -214,10 +214,15 @@ export const PrivateGameTemplateStoreStateProvider = ({
);
if (privateGameTemplateListingData) {
const priceForUsageType = privateGameTemplateListingData.prices.find(
price => price.usageType === 'default'
);
sendGameTemplateInformationOpened({
gameTemplateName: privateGameTemplateListingData.name,
gameTemplateId: privateGameTemplateListingData.id,
source: 'web-link',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
shopNavigationState.openPrivateGameTemplateInformationPage({
privateGameTemplateListingData,

View File

@@ -435,10 +435,16 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
if (!receivedAssetPack || (options && options.forceProductPage)) {
// The user has not received the pack, open the page to buy it.
const priceForUsageType = privateAssetPackListingData.prices.find(
price => price.usageType === 'default'
);
sendAssetPackInformationOpened({
assetPackName: privateAssetPackListingData.name,
assetPackId: privateAssetPackListingData.id,
assetPackKind: 'private',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
saveScrollPosition();
shopNavigationState.openPrivateAssetPackInformationPage({
@@ -489,10 +495,15 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
const selectPrivateGameTemplate = React.useCallback(
(privateGameTemplateListingData: PrivateGameTemplateListingData) => {
const priceForUsageType = privateGameTemplateListingData.prices.find(
price => price.usageType === 'default'
);
sendGameTemplateInformationOpened({
gameTemplateName: privateGameTemplateListingData.name,
gameTemplateId: privateGameTemplateListingData.id,
source: 'store',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
saveScrollPosition();
shopNavigationState.openPrivateGameTemplateInformationPage({
@@ -506,10 +517,15 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
const selectBundle = React.useCallback(
(bundleListingData: BundleListingData) => {
const priceForUsageType = bundleListingData.prices.find(
price => price.usageType === 'default'
);
sendBundleInformationOpened({
bundleName: bundleListingData.name,
bundleId: bundleListingData.id,
source: 'store',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
saveScrollPosition();
shopNavigationState.openBundleInformationPage({
@@ -523,10 +539,15 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
const selectCourse = React.useCallback(
(courseListingData: CourseListingData) => {
const priceForUsageType = courseListingData.prices.find(
price => price.usageType === 'default'
);
sendCourseInformationOpened({
courseName: courseListingData.name,
courseId: courseListingData.id,
source: 'store',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
if (onCourseOpen) onCourseOpen(courseListingData.id);
},

View File

@@ -7,7 +7,10 @@ import {
type Course,
type CourseChapter,
} from '../../../../Utils/GDevelopServices/Asset';
import type { BundleListingData } from '../../../../Utils/GDevelopServices/Shop';
import {
type BundleListingData,
type CourseListingData,
} from '../../../../Utils/GDevelopServices/Shop';
import CoursePreviewBanner from '../../../../Course/CoursePreviewBanner';
import type { CourseCompletion, CourseChapterCompletion } from '../UseCourses';
import { Line } from '../../../../UI/Grid';
@@ -36,7 +39,7 @@ const styles = {
type Props = {|
onBack: () => void,
courses: ?Array<Course>,
onSelectCourse: (courseId: string) => void,
onSelectCourse: (courseListingData: CourseListingData) => void,
onSelectBundle: (bundleListingData: BundleListingData) => void,
previewedCourse: ?Course,
getCourseChapters: (courseId: string) => ?Array<CourseChapter>,
@@ -83,8 +86,12 @@ const CoursesPage = ({
getCourseCompletion={getCourseCompletion}
getCourseChapterCompletion={getCourseChapterCompletion}
onDisplayCourse={() => {
if (!previewedCourse) return;
onSelectCourse(previewedCourse.id);
if (!previewedCourse || !listedCourses) return;
const courseListingData = listedCourses.find(
listedCourse => listedCourse.id === previewedCourse.id
);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
/>
</SectionRow>
@@ -109,7 +116,8 @@ const CoursesPage = ({
courseListingData={courseListingData}
completion={completion}
onClick={() => {
onSelectCourse(course.id);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
/>
</GridListTile>
@@ -159,7 +167,8 @@ const CoursesPage = ({
courseListingData={courseListingData}
completion={completion}
onClick={() => {
onSelectCourse(course.id);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
/>
</GridListTile>
@@ -201,7 +210,8 @@ const CoursesPage = ({
courseListingData={courseListingData}
completion={completion}
onClick={() => {
onSelectCourse(course.id);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
/>
</GridListTile>

View File

@@ -34,6 +34,7 @@ import ExampleStore from '../../../../AssetStore/ExampleStore';
import {
type PrivateGameTemplateListingData,
type BundleListingData,
type CourseListingData,
} from '../../../../Utils/GDevelopServices/Shop';
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
import Carousel from '../../../../UI/Carousel';
@@ -62,7 +63,7 @@ type Props = {|
onSelectCategory: (category: LearnCategory) => void,
selectInAppTutorial: (tutorialId: string) => void,
courses: ?(Course[]),
onSelectCourse: (courseId: string) => void,
onSelectCourse: (courseListingData: CourseListingData) => void,
onSelectBundle: (bundleListingData: BundleListingData) => void,
getCourseCompletion: (courseId: string) => CourseCompletion | null,
getCourseChapterCompletion: (
@@ -216,7 +217,8 @@ const MainPage = ({
courseListingData={courseListingData}
completion={completion}
onClick={() => {
onSelectCourse(course.id);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
/>
</GridListTile>

View File

@@ -21,6 +21,8 @@ import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
import Text from '../../../../UI/Text';
import { TutorialContext } from '../../../../Tutorial/TutorialContext';
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
import CourseStoreContext from '../../../../Course/CourseStoreContext';
import { type CourseListingData } from '../../../../Utils/GDevelopServices/Shop';
const styles = {
educationCurriculumTutorialContainer: {
@@ -143,7 +145,7 @@ type Props = {|
onBack: () => void,
category: TutorialCategory,
onOpenTemplateFromTutorial: string => Promise<void>,
onSelectCourse: (courseId: string) => void,
onSelectCourse: (courseListingData: CourseListingData) => void,
|};
const TutorialsCategoryPage = ({
@@ -152,6 +154,7 @@ const TutorialsCategoryPage = ({
onOpenTemplateFromTutorial,
onSelectCourse,
}: Props) => {
const { listedCourses } = React.useContext(CourseStoreContext);
const { limits } = React.useContext(AuthenticatedUserContext);
const { tutorials } = React.useContext(TutorialContext);
const texts = TUTORIAL_CATEGORY_TEXTS[category];
@@ -187,7 +190,14 @@ const TutorialsCategoryPage = ({
<EducationCurriculum
tutorials={filteredTutorials}
onSelectTutorial={setSelectedTutorial}
onSelectCourse={onSelectCourse}
onSelectCourse={courseId => {
if (!listedCourses) return;
const courseListingData = listedCourses.find(
course => course.id === courseId
);
if (!courseListingData) return;
onSelectCourse(courseListingData);
}}
i18n={i18n}
limits={limits}
onOpenTemplateFromTutorial={onOpenTemplateFromTutorial}

View File

@@ -119,10 +119,15 @@ const LearnSection = ({
const onOpenBundle = React.useCallback(
(bundleListingData: BundleListingData) => {
const priceForUsageType = bundleListingData.prices.find(
price => price.usageType === 'default'
);
sendBundleInformationOpened({
bundleName: bundleListingData.name,
bundleId: bundleListingData.id,
source: 'learn',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
setSelectedBundleListingData(bundleListingData);
},
@@ -130,18 +135,27 @@ const LearnSection = ({
);
const onOpenCourse = React.useCallback(
(courseId: string | null) => {
if (courseId && courses) {
const course = courses.find(c => c.id === courseId);
if (course && course.isLocked) {
// Only send the event if the course is not owned.
sendCourseInformationOpened({
courseName: course.titleByLocale['en'],
courseId: courseId,
source: 'learn',
});
}
(courseListingData: CourseListingData) => {
const courseId = courseListingData.id;
if (!courses) {
return;
}
const course = courses.find(c => c.id === courseId);
if (course && course.isLocked) {
const priceForUsageType = courseListingData.prices.find(
price => price.usageType === 'default'
);
// Only send the event if the course is not owned.
sendCourseInformationOpened({
courseName: courseListingData.name,
courseId: courseListingData.id,
source: 'learn',
priceValue: priceForUsageType && priceForUsageType.value,
priceCurrency: priceForUsageType && priceForUsageType.currency,
});
}
onSelectCourse(courseId);
},
[onSelectCourse, courses]
@@ -210,9 +224,7 @@ const LearnSection = ({
}}
onGameTemplateOpen={onSelectPrivateGameTemplateListingData}
onBundleOpen={onOpenBundle}
onCourseOpen={courseListingData => {
onOpenCourse(courseListingData.id);
}}
onCourseOpen={onOpenCourse}
courses={courses}
receivedCourses={receivedCourses}
getCourseCompletion={getCourseCompletion}

View File

@@ -405,8 +405,9 @@ const useCourses = () => {
sendCourseBuyClicked({
courseId: course.id,
courseName: course.titleByLocale.en,
currency: priceForUsageType ? priceForUsageType.currency : undefined,
usageType: 'default',
priceValue: priceForUsageType.value,
priceCurrency: priceForUsageType.currency,
});
setPurchasingCourseListingData(listedCourse);
@@ -433,13 +434,6 @@ const useCourses = () => {
return;
}
sendCourseBuyClicked({
courseId: course.id,
courseName: course.titleByLocale.en,
currency: 'CREDITS',
usageType: 'default',
});
const currentCreditsAmount = limits.credits.userBalance.amount;
const listedCourse = listedCourses.find(
listedCourse => listedCourse.id === course.id
@@ -458,6 +452,15 @@ const useCourses = () => {
return;
}
const creditsAmount = priceForUsageType.amount;
sendCourseBuyClicked({
courseId: course.id,
courseName: course.titleByLocale.en,
usageType: 'default',
priceValue: creditsAmount,
priceCurrency: 'CREDITS',
});
if (currentCreditsAmount < creditsAmount) {
openCreditsPackageDialog({
missingCredits: creditsAmount - currentCreditsAmount,

View File

@@ -13,6 +13,8 @@ import { getBrowserLanguageOrLocale } from '../Language';
import { type SubscriptionAnalyticsMetadata } from '../../Profile/Subscription/SubscriptionSuggestionContext';
import optionalRequire from '../OptionalRequire';
import Window from '../Window';
import { isMobile, isNativeMobileApp } from '../Platform';
import { retryIfFailed } from '../RetryIfFailed';
const electron = optionalRequire('electron');
const isElectronApp = !!electron;
@@ -26,6 +28,59 @@ let userIdentified = false;
let posthogLastPropertiesSent = '';
let currentlyRunningInAppTutorial = null;
let gdevelopEditorAnalytics: {|
initialize: (rootElement: HTMLElement) => Promise<void>,
identify: (
userId: string,
userProperties: { [string]: any }
) => Promise<void>,
trackEvent: (eventName: string, metadata: { [string]: any }) => Promise<void>,
|} | null = null;
let gdevelopEditorAnalyticsPromise: Promise<void> | null = null;
const ensureGDevelopEditorAnalyticsReady = async () => {
if (gdevelopEditorAnalytics) {
// Already loaded.
return;
}
if (gdevelopEditorAnalyticsPromise) {
// Being loaded.
return gdevelopEditorAnalyticsPromise;
}
gdevelopEditorAnalyticsPromise = (async () => {
try {
// Load the library. If it fails, retry or throw so we can retry later.
const module = await retryIfFailed(
{ times: 2 },
async () =>
// $FlowExpectedError - Remote script cannot be found.
(await import(/* webpackIgnore: true */ 'https://resources.gdevelop.io/a/gea.js'))
.default
);
if (module) {
await module.initialize({
documentBody: document.body,
isNativeMobileApp: isNativeMobileApp(),
isElectronApp,
isDev,
isMobile: isMobile(),
ideVersionWithHash: getIDEVersionWithHash(),
});
gdevelopEditorAnalytics = module;
}
} catch (error) {
console.error('Error while loading GDevelop Editor Analytics:', error);
} finally {
// If loading fails, retry later.
gdevelopEditorAnalyticsPromise = null;
}
})();
return gdevelopEditorAnalyticsPromise;
};
export const setCurrentlyRunningInAppTutorial = (tutorial: string | null) =>
(currentlyRunningInAppTutorial = tutorial);
@@ -55,24 +110,33 @@ const recordEvent = (name: string, metadata?: { [string]: any }) => {
return;
}
if (!posthogLoaded || !userIdentified) {
console.info(`App analytics not ready for an event - retrying in 2s.`);
setTimeout(() => {
console.info(
`Retrying to send the app analytics event with name ${name}`
);
recordEvent(name, metadata);
}, 2000);
(() => {
if (!posthogLoaded || !userIdentified) {
console.info(`App analytics not ready for an event - retrying in 2s.`);
setTimeout(() => {
console.info(
`Retrying to send the app analytics event with name ${name}`
);
recordEvent(name, metadata);
}, 2000);
return;
}
return;
}
posthog.capture(name, {
...metadata,
isInAppTutorialRunning: currentlyRunningInAppTutorial,
isInDesktopApp: isElectronApp,
isInWebApp: !isElectronApp,
});
posthog.capture(name, {
...metadata,
isInAppTutorialRunning: currentlyRunningInAppTutorial,
isInDesktopApp: isElectronApp,
isInWebApp: !isElectronApp,
});
})();
(async () => {
await ensureGDevelopEditorAnalyticsReady();
if (gdevelopEditorAnalytics) {
await gdevelopEditorAnalytics.trackEvent(name, metadata || {});
}
})();
};
/**
@@ -84,6 +148,10 @@ export const installAnalyticsEvents = () => {
return;
}
ensureGDevelopEditorAnalyticsReady().catch(() => {
// Will be retried when an event is sent.
});
posthog.init('phc_yjTVz4BMHUOhCLBhVImjk3Jn1AjMCg808bxENY228qu', {
api_host: 'https://app.posthog.com',
loaded: () => {
@@ -107,16 +175,6 @@ export const identifyUserForAnalytics = (
return;
}
if (!posthogLoaded) {
console.info(`App analytics not ready - retrying in 2s.`);
setTimeout(() => {
console.info(`Retrying to update the user for app analytics.`);
identifyUserForAnalytics(authenticatedUser);
}, 2000);
return;
}
const firebaseUser = authenticatedUser.firebaseUser;
const profile = authenticatedUser.profile;
const userPreferences = loadPreferencesFromLocalStorage();
@@ -145,19 +203,39 @@ export const identifyUserForAnalytics = (
hearFrom: profile ? profile.hearFrom : undefined,
};
// Identify which user is using the app, after de-duplicating the call to
// avoid useless calls.
// This is so we can build stats on the used version, languages and usage
// of GDevelop features.
const stringifiedUserProperties = JSON.stringify(userProperties);
if (stringifiedUserProperties !== posthogLastPropertiesSent) {
// If the user is not logged in, identify the user by its anonymous UUID.
// If the user is logged in, identify the user by its Firebase ID.
const userId = firebaseUser ? firebaseUser.uid : getUserUUID();
posthog.identify(userId, userProperties);
posthogLastPropertiesSent = stringifiedUserProperties;
userIdentified = true;
}
// If the user is not logged in, identify the user by its anonymous UUID.
// If the user is logged in, identify the user by its Firebase ID.
const userId = firebaseUser ? firebaseUser.uid : getUserUUID();
(() => {
if (!posthogLoaded) {
console.info(`App analytics not ready - retrying in 2s.`);
setTimeout(() => {
console.info(`Retrying to update the user for app analytics.`);
identifyUserForAnalytics(authenticatedUser);
}, 2000);
return;
}
// Identify which user is using the app, after de-duplicating the call to
// avoid useless calls.
// This is so we can build stats on the used version, languages and usage
// of GDevelop features.
const stringifiedUserProperties = JSON.stringify(userProperties);
if (stringifiedUserProperties !== posthogLastPropertiesSent) {
posthog.identify(userId, userProperties);
posthogLastPropertiesSent = stringifiedUserProperties;
userIdentified = true;
}
})();
(async () => {
await ensureGDevelopEditorAnalyticsReady();
if (gdevelopEditorAnalytics) {
await gdevelopEditorAnalytics.identify(userId, userProperties);
}
})();
};
/**
@@ -261,6 +339,13 @@ export const sendInAppTutorialExited = (metadata: {|
recordEvent('in-app-tutorial-exited', metadata);
};
const patchWithCurrencyField = (options: { [string]: any }) => {
return {
...options,
currency: options.priceCurrency,
};
};
export const sendAssetPackOpened = (options: {|
assetPackId: string | null,
assetPackName: string,
@@ -276,70 +361,85 @@ export const sendAssetPackBuyClicked = (options: {|
assetPackName: string,
assetPackTag: string,
assetPackKind: 'public' | 'private' | 'unknown',
currency?: string,
priceValue: number | void,
priceCurrency: string | void,
usageType: string,
|}) => {
recordEvent('asset_pack_buy_clicked', options);
recordEvent('asset_pack_buy_clicked', patchWithCurrencyField(options));
};
export const sendAssetPackInformationOpened = (options: {|
assetPackId: string,
assetPackName: string,
assetPackKind: 'public' | 'private' | 'unknown',
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('asset_pack_information_opened', options);
recordEvent('asset_pack_information_opened', patchWithCurrencyField(options));
};
export const sendGameTemplateBuyClicked = (options: {|
gameTemplateId: string,
gameTemplateName: string,
gameTemplateTag: string,
currency?: string,
usageType: string,
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('game_template_buy_clicked', options);
recordEvent('game_template_buy_clicked', patchWithCurrencyField(options));
};
export const sendGameTemplateInformationOpened = (options: {|
gameTemplateId: string,
gameTemplateName: string,
source: 'store' | 'examples-list' | 'homepage' | 'web-link',
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('game_template_information_opened', options);
recordEvent(
'game_template_information_opened',
patchWithCurrencyField(options)
);
};
export const sendBundleBuyClicked = (options: {|
bundleId: string,
bundleName: string,
bundleTag: string,
currency?: string,
usageType: string,
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('bundle_buy_clicked', options);
recordEvent('bundle_buy_clicked', patchWithCurrencyField(options));
};
export const sendBundleInformationOpened = (options: {|
bundleId: string,
bundleName: string,
source: 'store' | 'learn' | 'web-link',
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('bundle_information_opened', options);
recordEvent('bundle_information_opened', patchWithCurrencyField(options));
};
export const sendCourseInformationOpened = (options: {|
courseId: string,
courseName: string,
source: 'store' | 'learn',
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('course_information_opened', options);
recordEvent('course_information_opened', patchWithCurrencyField(options));
};
export const sendCourseBuyClicked = (options: {|
courseId: string,
courseName: string,
currency?: string,
usageType: string,
priceValue: number | void,
priceCurrency: string | void,
|}) => {
recordEvent('course_buy_clicked', options);
recordEvent('course_buy_clicked', patchWithCurrencyField(options));
};
export const sendUserSurveyStarted = () => {