mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
1 Commits
integratio
...
game-ad-ea
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7ff6fee1b |
@@ -57,10 +57,12 @@ const CustomTooltip = ({
|
||||
payload,
|
||||
label,
|
||||
customStyle,
|
||||
decimals = 2,
|
||||
}: {|
|
||||
payload: ?Array<any>,
|
||||
label: string,
|
||||
customStyle: Object,
|
||||
decimals?: number,
|
||||
|}) =>
|
||||
payload ? (
|
||||
<Paper style={customStyle} background="light">
|
||||
@@ -79,7 +81,9 @@ const CustomTooltip = ({
|
||||
index
|
||||
) => (
|
||||
<Text noMargin key={index}>{`${name}: ${
|
||||
Number.isInteger(value) ? value.toString() : value.toFixed(2)
|
||||
Number.isInteger(value)
|
||||
? value.toString()
|
||||
: value.toFixed(decimals)
|
||||
}${unit ? ` ${unit}` : ''}`}</Text>
|
||||
)
|
||||
)}
|
||||
@@ -397,3 +401,53 @@ export const PlayersDurationPerDayChart = ({
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameAdEarningsChart = ({
|
||||
i18n,
|
||||
chartData,
|
||||
height,
|
||||
fontSize,
|
||||
}: ChartProps) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const styles = getChartsStyleFromTheme(gdevelopTheme);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width={chartWidth} height={height} debounce={1}>
|
||||
<AreaChart data={chartData.adsEarnings} margin={chartMargins}>
|
||||
<Area
|
||||
name={i18n._(t`USD`)}
|
||||
type="monotone"
|
||||
dataKey="accumulatedEarningsInUSDs"
|
||||
stroke={gdevelopTheme.chart.dataColor1}
|
||||
fill={gdevelopTheme.chart.dataColor1}
|
||||
fillOpacity={0.25}
|
||||
yAxisId={0}
|
||||
/>
|
||||
<CartesianGrid
|
||||
stroke={gdevelopTheme.chart.gridColor}
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke={gdevelopTheme.chart.textColor}
|
||||
style={styles.tickLabel}
|
||||
tick={{ fontSize: fontSize === 'small' ? 12 : 16 }}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="accumulatedEarningsInUSDs"
|
||||
stroke={gdevelopTheme.chart.textColor}
|
||||
style={styles.tickLabel}
|
||||
tick={{ fontSize: fontSize === 'small' ? 12 : 16 }}
|
||||
/>
|
||||
<Tooltip
|
||||
content={props =>
|
||||
CustomTooltip({
|
||||
...props,
|
||||
customStyle: styles.tooltipContent,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
@@ -7,13 +7,31 @@ import {
|
||||
addDays,
|
||||
} from 'date-fns';
|
||||
import { type GameMetrics } from '../Utils/GDevelopServices/Analytics';
|
||||
import {
|
||||
type GameAdEarning,
|
||||
type Usage,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
export type MergedGameMetrics = GameMetrics & {
|
||||
export type MergedGameMetrics = {|
|
||||
...GameMetrics,
|
||||
/**
|
||||
* The start date is not defined when only one day is merged.
|
||||
*/
|
||||
startDate: string | null,
|
||||
};
|
||||
|};
|
||||
|
||||
export type MergedGameAdEarnings = {|
|
||||
...GameAdEarning,
|
||||
/**
|
||||
* The start date is not defined when only one day is merged.
|
||||
*/
|
||||
startDate: string | null,
|
||||
|};
|
||||
|
||||
export type CashOuts = {|
|
||||
date: string,
|
||||
amountInCredits: number,
|
||||
|};
|
||||
|
||||
/**
|
||||
* It's divisible by 7.
|
||||
@@ -46,6 +64,8 @@ export type ChartData = {|
|
||||
playersPercent: number,
|
||||
durationInMinutes: number,
|
||||
|},
|
||||
|
||||
totalEarningsInUSDs: number,
|
||||
|},
|
||||
/**
|
||||
* Metrics for each day of a month or each week of a year.
|
||||
@@ -92,6 +112,14 @@ export type ChartData = {|
|
||||
* A funnel of the remaining players after a given played duration.
|
||||
*/
|
||||
overPlayedDuration: {| duration: number, playersCount: number |}[],
|
||||
/**
|
||||
* Accumulated earnings from ads each day.
|
||||
*/
|
||||
adsEarnings: {|
|
||||
date: string,
|
||||
accumulatedEarningsInCredits: number,
|
||||
accumulatedEarningsInUSDs: number,
|
||||
|}[],
|
||||
|};
|
||||
|
||||
const emptyChartData: ChartData = {
|
||||
@@ -110,9 +138,11 @@ const emptyChartData: ChartData = {
|
||||
playersPercent: 0,
|
||||
durationInMinutes: 0,
|
||||
},
|
||||
totalEarningsInUSDs: 0,
|
||||
},
|
||||
overTime: [],
|
||||
overPlayedDuration: [],
|
||||
adsEarnings: [],
|
||||
};
|
||||
|
||||
const durationIndexes: { [string]: number } = {
|
||||
@@ -124,9 +154,9 @@ const durationIndexes: { [string]: number } = {
|
||||
};
|
||||
export const durationValues = [1, 3, 5, 10, 15];
|
||||
|
||||
const createZeroesMetric = (date: Date): GameMetrics => {
|
||||
const createZeroesGameMetric = (date: Date): GameMetrics => {
|
||||
return {
|
||||
date: formatISO(date),
|
||||
date: formatISO(date, { representation: 'date' }),
|
||||
|
||||
sessions: {
|
||||
d0Sessions: 0,
|
||||
@@ -150,10 +180,15 @@ const createZeroesMetric = (date: Date): GameMetrics => {
|
||||
* @param gameMetrics concise game metrics from the backend (today first)
|
||||
* @returns game metrics with a metric for each 364 past days (today first).
|
||||
*/
|
||||
const fillMissingDays = (
|
||||
const fillMissingGameMetricsDays = ({
|
||||
gameMetrics,
|
||||
todayDate,
|
||||
totalDays,
|
||||
}: {
|
||||
gameMetrics: Array<GameMetrics>,
|
||||
todayDate: Date
|
||||
): Array<GameMetrics> => {
|
||||
todayDate: Date,
|
||||
totalDays: number,
|
||||
}): Array<GameMetrics> => {
|
||||
const filledGameMetrics = [];
|
||||
// TODO In some timezones, it might start the wrong day.
|
||||
let previousMetricDate = addDays(todayDate, 1);
|
||||
@@ -164,21 +199,77 @@ const fillMissingDays = (
|
||||
differenceInCalendarDays(parseISO(metric.date), previousMetricDate) < -1
|
||||
) {
|
||||
const addedMetricDate = subDays(previousMetricDate, 1);
|
||||
filledGameMetrics.push(createZeroesMetric(addedMetricDate));
|
||||
filledGameMetrics.push(createZeroesGameMetric(addedMetricDate));
|
||||
previousMetricDate = addedMetricDate;
|
||||
}
|
||||
filledGameMetrics.push(metric);
|
||||
previousMetricDate = metricDate;
|
||||
}
|
||||
// Fill to one year
|
||||
while (filledGameMetrics.length < daysShownForYear) {
|
||||
// Fill to total days
|
||||
while (filledGameMetrics.length < totalDays) {
|
||||
const addedMetricDate = subDays(previousMetricDate, 1);
|
||||
filledGameMetrics.push(createZeroesMetric(addedMetricDate));
|
||||
filledGameMetrics.push(createZeroesGameMetric(addedMetricDate));
|
||||
previousMetricDate = addedMetricDate;
|
||||
}
|
||||
return filledGameMetrics;
|
||||
};
|
||||
|
||||
const createZeroesGameAdEarning = (
|
||||
gameId: string,
|
||||
date: Date
|
||||
): GameAdEarning => {
|
||||
return {
|
||||
gameId,
|
||||
date: formatISO(date, { representation: 'date' }),
|
||||
adEarningsInCredits: 0,
|
||||
adEarningsInMilliUSDs: 0,
|
||||
updatedAt: date.getTime(),
|
||||
};
|
||||
};
|
||||
|
||||
const fillMissingGameAdEarningsDays = ({
|
||||
gameAdEarnings,
|
||||
todayDate,
|
||||
totalDays,
|
||||
gameId,
|
||||
}: {
|
||||
gameAdEarnings: Array<GameAdEarning>,
|
||||
todayDate: Date,
|
||||
totalDays: number,
|
||||
gameId: string,
|
||||
}): Array<GameAdEarning> => {
|
||||
const filledGameAdEarnings = [];
|
||||
let previousEarningDate = addDays(todayDate, 1);
|
||||
for (const earning of gameAdEarnings) {
|
||||
const earningDate = parseISO(earning.date);
|
||||
// Fill holes
|
||||
while (
|
||||
differenceInCalendarDays(parseISO(earning.date), previousEarningDate) < -1
|
||||
) {
|
||||
const addedEarningDate = subDays(previousEarningDate, 1);
|
||||
filledGameAdEarnings.push(
|
||||
createZeroesGameAdEarning(gameId, addedEarningDate)
|
||||
);
|
||||
previousEarningDate = addedEarningDate;
|
||||
}
|
||||
filledGameAdEarnings.push(earning);
|
||||
previousEarningDate = earningDate;
|
||||
}
|
||||
|
||||
// Fill to total days
|
||||
while (filledGameAdEarnings.length < totalDays) {
|
||||
const addedEarningDate = subDays(previousEarningDate, 1);
|
||||
filledGameAdEarnings.push(
|
||||
createZeroesGameAdEarning(gameId, addedEarningDate)
|
||||
);
|
||||
previousEarningDate = addedEarningDate;
|
||||
}
|
||||
|
||||
console.log('filledGameAdEarnings', filledGameAdEarnings);
|
||||
|
||||
return filledGameAdEarnings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sum each metric or `undefined` when one side is `undefined`.
|
||||
* When one metric is `undefined`, the value of the other is not used because
|
||||
@@ -192,8 +283,8 @@ const fillMissingDays = (
|
||||
* @returns the sum for each metric or `undefined` when one side is `undefined`
|
||||
*/
|
||||
const mergeGameMetrics = (
|
||||
a: GameMetrics,
|
||||
b: GameMetrics
|
||||
a: GameMetrics | MergedGameMetrics,
|
||||
b: GameMetrics | MergedGameMetrics
|
||||
): MergedGameMetrics => {
|
||||
return {
|
||||
date: a.date,
|
||||
@@ -269,6 +360,44 @@ const mergeGameMetricsByWeek = (
|
||||
return mergedGameMetrics;
|
||||
};
|
||||
|
||||
const mergeGameAdEarnings = (
|
||||
a: GameAdEarning | MergedGameAdEarnings,
|
||||
b: GameAdEarning | MergedGameAdEarnings
|
||||
): MergedGameAdEarnings => {
|
||||
return {
|
||||
date: a.date,
|
||||
startDate: b.date,
|
||||
|
||||
gameId: a.gameId,
|
||||
adEarningsInCredits: a.adEarningsInCredits + b.adEarningsInCredits,
|
||||
adEarningsInMilliUSDs: a.adEarningsInMilliUSDs + b.adEarningsInMilliUSDs,
|
||||
updatedAt: a.updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
const mergeGameAdEarningsByWeek = (
|
||||
gameAdEarnings: GameAdEarning[]
|
||||
): MergedGameAdEarnings[] => {
|
||||
const mergedGameAdEarnings: Array<MergedGameAdEarnings> = [];
|
||||
for (let weekIndex = 0; weekIndex < gameAdEarnings.length; weekIndex += 7) {
|
||||
let mergedGameAdEarning = gameAdEarnings[weekIndex];
|
||||
for (
|
||||
let index = weekIndex + 1;
|
||||
index < weekIndex + 7 && index < gameAdEarnings.length;
|
||||
index++
|
||||
) {
|
||||
mergedGameAdEarning = mergeGameAdEarnings(
|
||||
mergedGameAdEarning,
|
||||
gameAdEarnings[index]
|
||||
);
|
||||
}
|
||||
mergedGameAdEarnings.push(
|
||||
((mergedGameAdEarning: any): MergedGameAdEarnings)
|
||||
);
|
||||
}
|
||||
return mergedGameAdEarnings;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param playersBelowSums
|
||||
* @param playersCount
|
||||
@@ -350,13 +479,36 @@ const subtract = (a: ?number, b: ?number): number => {
|
||||
* @returns enriched game metrics that are ready to be used in a chart
|
||||
* (today first).
|
||||
*/
|
||||
const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
const evaluateChartData = ({
|
||||
allMergedGameMetrics,
|
||||
allMergedGameAdEarnings,
|
||||
cashOuts,
|
||||
period,
|
||||
}: {
|
||||
allMergedGameMetrics: MergedGameMetrics[],
|
||||
allMergedGameAdEarnings: MergedGameAdEarnings[],
|
||||
cashOuts: CashOuts[],
|
||||
period: 'week' | 'month' | 'year',
|
||||
}): ChartData => {
|
||||
let playersBelowSums = [0, 0, 0, 0, 0];
|
||||
let playersSum = 0;
|
||||
let onlyFullyDefinedPlayersSum = 0;
|
||||
let playedDurationSumInMinutes = 0;
|
||||
|
||||
metrics.forEach(metric => {
|
||||
const numberOfItemsInPeriod =
|
||||
period === 'week'
|
||||
? 7
|
||||
: period === 'month'
|
||||
? 30
|
||||
: // merged by week
|
||||
52;
|
||||
|
||||
const gameMetricsForPeriod = allMergedGameMetrics.slice(
|
||||
0,
|
||||
numberOfItemsInPeriod
|
||||
);
|
||||
|
||||
gameMetricsForPeriod.forEach(metric => {
|
||||
const d0SessionsDurationTotal =
|
||||
metric.sessions && metric.sessions.d0SessionsDurationTotal !== null
|
||||
? metric.sessions.d0SessionsDurationTotal
|
||||
@@ -412,9 +564,9 @@ const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
const dateFormatOptions = { month: 'short', day: 'numeric' };
|
||||
const noMonthDateFormatOptions = { day: 'numeric' };
|
||||
|
||||
const formatDate = (metric: MergedGameMetrics) => {
|
||||
const startIsoDate = metric.startDate;
|
||||
const endDate = parseISO(metric.date);
|
||||
const formatDate = (chartItem: MergedGameMetrics | MergedGameAdEarnings) => {
|
||||
const startIsoDate = chartItem.startDate;
|
||||
const endDate = parseISO(chartItem.date);
|
||||
const formattedDate = endDate.toLocaleDateString(
|
||||
undefined,
|
||||
dateFormatOptions
|
||||
@@ -435,6 +587,56 @@ const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
);
|
||||
};
|
||||
|
||||
const sortedGameAdEarnings = allMergedGameAdEarnings.sort(
|
||||
(a, b) => parseISO(a.date).getTime() - parseISO(b.date).getTime()
|
||||
);
|
||||
console.log('sortedGameAdEarnings', sortedGameAdEarnings);
|
||||
let accumulatedEarningsInCredits = 0;
|
||||
let accumulatedEarningsInMilliUSDs = 0;
|
||||
const sortedAndAccumulatedGameAdEarnings = sortedGameAdEarnings
|
||||
.map((earning, index) => {
|
||||
let accumulatedEarningsInCreditsToThatDay =
|
||||
earning.adEarningsInCredits + accumulatedEarningsInCredits;
|
||||
let accumulatedEarningsInMilliUSDsToThatDay =
|
||||
earning.adEarningsInMilliUSDs + accumulatedEarningsInMilliUSDs;
|
||||
|
||||
const cashOutsOnThatPeriod = cashOuts.filter(
|
||||
cashOut =>
|
||||
cashOut.date === earning.date ||
|
||||
(!!earning.startDate &&
|
||||
cashOut.date >= earning.startDate &&
|
||||
cashOut.date <= earning.date)
|
||||
);
|
||||
if (cashOutsOnThatPeriod.length) {
|
||||
cashOutsOnThatPeriod.forEach(cashOut => {
|
||||
console.log(accumulatedEarningsInCreditsToThatDay);
|
||||
const estimatedCreditToMilliUSDsRatio =
|
||||
accumulatedEarningsInMilliUSDsToThatDay /
|
||||
accumulatedEarningsInCreditsToThatDay;
|
||||
const cashOutInMilliUSDs = Math.floor(
|
||||
cashOut.amountInCredits * estimatedCreditToMilliUSDsRatio
|
||||
);
|
||||
|
||||
accumulatedEarningsInCreditsToThatDay -= cashOut.amountInCredits;
|
||||
accumulatedEarningsInMilliUSDsToThatDay -= cashOutInMilliUSDs;
|
||||
console.log(
|
||||
cashOut,
|
||||
estimatedCreditToMilliUSDsRatio,
|
||||
cashOutInMilliUSDs
|
||||
);
|
||||
});
|
||||
}
|
||||
accumulatedEarningsInCredits = accumulatedEarningsInCreditsToThatDay;
|
||||
accumulatedEarningsInMilliUSDs = accumulatedEarningsInMilliUSDsToThatDay;
|
||||
return {
|
||||
date: formatDate(earning),
|
||||
accumulatedEarningsInCredits: accumulatedEarningsInCreditsToThatDay,
|
||||
accumulatedEarningsInUSDs:
|
||||
Math.floor(accumulatedEarningsInMilliUSDsToThatDay / 10) / 100,
|
||||
};
|
||||
})
|
||||
.slice(allMergedGameAdEarnings.length - numberOfItemsInPeriod);
|
||||
|
||||
return {
|
||||
overview: {
|
||||
// Players from before the migration are shown as viewers
|
||||
@@ -470,8 +672,10 @@ const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
: 0,
|
||||
durationInMinutes: durationValues[greaterDurationPlayerIndex],
|
||||
},
|
||||
totalEarningsInUSDs:
|
||||
Math.floor(accumulatedEarningsInMilliUSDs / 10) / 100,
|
||||
},
|
||||
overTime: metrics
|
||||
overTime: gameMetricsForPeriod
|
||||
.map(metric => {
|
||||
const d0SessionsDurationTotal =
|
||||
metric.sessions && metric.sessions.d0SessionsDurationTotal !== null
|
||||
@@ -609,6 +813,7 @@ const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
};
|
||||
})
|
||||
),
|
||||
adsEarnings: sortedAndAccumulatedGameAdEarnings,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -617,55 +822,87 @@ const evaluateChartData = (metrics: MergedGameMetrics[]): ChartData => {
|
||||
* @returns enriched game metrics that are ready to be used in a chart
|
||||
* (today at last).
|
||||
*/
|
||||
export const buildChartData = (
|
||||
export const buildChartData = ({
|
||||
gameId,
|
||||
gameMetrics,
|
||||
gameAdEarnings,
|
||||
usages,
|
||||
todayDate = new Date(),
|
||||
}: {
|
||||
gameId: string,
|
||||
gameMetrics: ?Array<GameMetrics>,
|
||||
todayDate: Date = new Date()
|
||||
): { yearChartData: ChartData, monthChartData: ChartData } => {
|
||||
if (!gameMetrics) {
|
||||
gameAdEarnings: ?Array<GameAdEarning>,
|
||||
usages: ?Array<Usage>,
|
||||
todayDate?: Date,
|
||||
}): {|
|
||||
yearChartData: ChartData,
|
||||
monthChartData: ChartData,
|
||||
weekChartData: ChartData,
|
||||
|} => {
|
||||
if (!gameMetrics || !gameAdEarnings || !usages) {
|
||||
return {
|
||||
yearChartData: emptyChartData,
|
||||
monthChartData: emptyChartData,
|
||||
weekChartData: emptyChartData,
|
||||
};
|
||||
}
|
||||
const filledGameRollingMetrics = fillMissingDays(
|
||||
gameMetrics.sort(
|
||||
|
||||
const filledGameMetrics = fillMissingGameMetricsDays({
|
||||
gameMetrics: gameMetrics.sort(
|
||||
(a, b) => parseISO(b.date).getTime() - parseISO(a.date).getTime()
|
||||
),
|
||||
todayDate
|
||||
todayDate,
|
||||
totalDays: daysShownForYear,
|
||||
});
|
||||
const filledGameAdEarnings = fillMissingGameAdEarningsDays({
|
||||
gameAdEarnings: gameAdEarnings.sort(
|
||||
(a, b) => parseISO(b.date).getTime() - parseISO(a.date).getTime()
|
||||
),
|
||||
todayDate,
|
||||
totalDays: daysShownForYear,
|
||||
gameId,
|
||||
});
|
||||
const gameMetricsMergedByWeek = mergeGameMetricsByWeek(filledGameMetrics);
|
||||
const gameAdEarningsMergedByWeek = mergeGameAdEarningsByWeek(
|
||||
filledGameAdEarnings
|
||||
);
|
||||
|
||||
const gameMetricsMergedByDay: MergedGameMetrics[] = filledGameMetrics.map(
|
||||
metric => ({ ...metric, startDate: null })
|
||||
);
|
||||
const gameAdEarningsMergedByDay: MergedGameAdEarnings[] = filledGameAdEarnings.map(
|
||||
earning => ({ ...earning, startDate: null })
|
||||
);
|
||||
|
||||
const cashOuts = usages
|
||||
.filter(
|
||||
usage =>
|
||||
usage.type === 'change-balance' &&
|
||||
usage.description === 'Cash out of game earnings'
|
||||
)
|
||||
.map(usage => ({
|
||||
date: formatISO(usage.createdAt, { representation: 'date' }),
|
||||
amountInCredits: -(usage.creditsPaid || 0),
|
||||
}));
|
||||
|
||||
return {
|
||||
yearChartData: evaluateChartData(
|
||||
mergeGameMetricsByWeek(filledGameRollingMetrics)
|
||||
),
|
||||
monthChartData: evaluateChartData(
|
||||
filledGameRollingMetrics
|
||||
.slice(0, 30)
|
||||
.map(metric => ({ ...metric, startDate: null }: MergedGameMetrics))
|
||||
),
|
||||
yearChartData: evaluateChartData({
|
||||
allMergedGameMetrics: gameMetricsMergedByWeek,
|
||||
allMergedGameAdEarnings: gameAdEarningsMergedByWeek,
|
||||
cashOuts,
|
||||
period: 'year',
|
||||
}),
|
||||
monthChartData: evaluateChartData({
|
||||
allMergedGameMetrics: gameMetricsMergedByDay,
|
||||
allMergedGameAdEarnings: gameAdEarningsMergedByDay,
|
||||
cashOuts,
|
||||
period: 'month',
|
||||
}),
|
||||
weekChartData: evaluateChartData({
|
||||
allMergedGameMetrics: gameMetricsMergedByDay,
|
||||
allMergedGameAdEarnings: gameAdEarningsMergedByDay,
|
||||
cashOuts,
|
||||
period: 'week',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param gameMetrics concise game metrics from the backend (today first)
|
||||
* @returns enriched game metrics that are ready to be used in a chart
|
||||
* (today at last).
|
||||
*/
|
||||
export const buildLastWeekChartData = (
|
||||
gameMetrics: ?Array<GameMetrics>,
|
||||
todayDate: Date = new Date()
|
||||
): ChartData => {
|
||||
if (!gameMetrics) {
|
||||
return emptyChartData;
|
||||
}
|
||||
const filledGameRollingMetrics = fillMissingDays(
|
||||
gameMetrics.sort(
|
||||
(a, b) => parseISO(b.date).getTime() - parseISO(a.date).getTime()
|
||||
),
|
||||
todayDate
|
||||
);
|
||||
return evaluateChartData(
|
||||
filledGameRollingMetrics
|
||||
.slice(0, 7)
|
||||
.map(metric => ({ ...metric, startDate: null }: MergedGameMetrics))
|
||||
);
|
||||
};
|
||||
|
@@ -28,8 +28,13 @@ import {
|
||||
PlayersRepartitionPerDurationChart,
|
||||
PlayersDurationPerDayChart,
|
||||
SessionsChart,
|
||||
GameAdEarningsChart,
|
||||
} from './GameAnalyticsCharts';
|
||||
import MarketingPlanSingleDisplay from '../MarketingPlans/MarketingPlanSingleDisplay';
|
||||
import {
|
||||
getGameAdEarnings,
|
||||
type GameAdEarning,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
const chartHeight = 300;
|
||||
|
||||
@@ -46,62 +51,79 @@ export const GameAnalyticsPanel = ({
|
||||
gameFeaturings,
|
||||
fetchGameFeaturings,
|
||||
}: Props) => {
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
const { getAuthorizationHeader, profile, usages } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const [gameRollingMetrics, setGameMetrics] = React.useState<?(GameMetrics[])>(
|
||||
null
|
||||
);
|
||||
const [gameMetrics, setGameMetrics] = React.useState<?(GameMetrics[])>(null);
|
||||
const [
|
||||
gameAdEarnings,
|
||||
setGameAdEarnings,
|
||||
] = React.useState<?(GameAdEarning[])>(null);
|
||||
const { yearChartData, monthChartData } = React.useMemo(
|
||||
() => buildChartData(gameRollingMetrics),
|
||||
[gameRollingMetrics]
|
||||
() =>
|
||||
buildChartData({ gameMetrics, gameAdEarnings, usages, gameId: game.id }),
|
||||
[gameMetrics, gameAdEarnings, usages, game.id]
|
||||
);
|
||||
const [dataPeriod, setDataPeriod] = React.useState('month');
|
||||
const chartData = dataPeriod === 'year' ? yearChartData : monthChartData;
|
||||
|
||||
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
|
||||
null
|
||||
);
|
||||
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
// TODO In some timezones, it might ask one less or extra day.
|
||||
const lastYearIsoDate = formatISO(subDays(new Date(), daysShownForYear), {
|
||||
representation: 'date',
|
||||
});
|
||||
const loadGameMetrics = React.useCallback(
|
||||
const loadGameAnalytics = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
const { id } = profile;
|
||||
|
||||
setIsGameMetricsLoading(true);
|
||||
setGameMetricsError(null);
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const lastYearIsoDate = formatISO(subDays(new Date(), daysShownForYear), {
|
||||
representation: 'date',
|
||||
});
|
||||
const gameCreatioDateIsoDate = formatISO(new Date(game.createdAt), {
|
||||
representation: 'date',
|
||||
});
|
||||
const todayIsoDate = formatISO(new Date(), {
|
||||
representation: 'date',
|
||||
});
|
||||
|
||||
try {
|
||||
const gameRollingMetrics = await getGameMetricsFrom(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
lastYearIsoDate
|
||||
);
|
||||
const [gameRollingMetrics, gameAdEarnings] = await Promise.all([
|
||||
getGameMetricsFrom(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
lastYearIsoDate
|
||||
),
|
||||
getGameAdEarnings(getAuthorizationHeader, id, {
|
||||
gameId: game.id,
|
||||
startIsoDate: gameCreatioDateIsoDate,
|
||||
endIsoDate: todayIsoDate,
|
||||
}),
|
||||
]);
|
||||
setGameMetrics(gameRollingMetrics);
|
||||
setGameAdEarnings(gameAdEarnings);
|
||||
} catch (err) {
|
||||
console.error(`Unable to load game rolling metrics:`, err);
|
||||
setGameMetricsError(err);
|
||||
setError(err);
|
||||
}
|
||||
setIsGameMetricsLoading(false);
|
||||
setIsLoading(false);
|
||||
},
|
||||
[getAuthorizationHeader, profile, game, lastYearIsoDate]
|
||||
[getAuthorizationHeader, profile, game]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
loadGameMetrics();
|
||||
loadGameAnalytics();
|
||||
},
|
||||
[loadGameMetrics]
|
||||
[loadGameAnalytics]
|
||||
);
|
||||
|
||||
if (isGameMetricsLoading) return <PlaceholderLoader />;
|
||||
if (isLoading) return <PlaceholderLoader />;
|
||||
|
||||
const displaySuggestedMarketingPlan =
|
||||
recommendedMarketingPlan && gameFeaturings && fetchGameFeaturings;
|
||||
@@ -109,10 +131,10 @@ export const GameAnalyticsPanel = ({
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) =>
|
||||
gameRollingMetricsError ? (
|
||||
error ? (
|
||||
<PlaceholderError
|
||||
onRetry={() => {
|
||||
loadGameMetrics();
|
||||
loadGameAnalytics();
|
||||
}}
|
||||
>
|
||||
<Trans>There was an issue getting the game analytics.</Trans>{' '}
|
||||
@@ -136,8 +158,28 @@ export const GameAnalyticsPanel = ({
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={displaySuggestedMarketingPlan ? 7 : 12}
|
||||
md={displaySuggestedMarketingPlan ? 8 : 12}
|
||||
sm={displaySuggestedMarketingPlan ? 4 : 12}
|
||||
md={displaySuggestedMarketingPlan ? 4 : 12}
|
||||
>
|
||||
<Column noMargin alignItems="center" expand>
|
||||
<Text size="block-title" align="center">
|
||||
<Trans>
|
||||
USD {chartData.overview.totalEarningsInUSDs} in Ads
|
||||
earnings
|
||||
</Trans>
|
||||
</Text>
|
||||
<GameAdEarningsChart
|
||||
chartData={chartData}
|
||||
height={chartHeight}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={displaySuggestedMarketingPlan ? 4 : 12}
|
||||
md={displaySuggestedMarketingPlan ? 4 : 12}
|
||||
>
|
||||
<Column noMargin alignItems="center" expand>
|
||||
<Text size="block-title" align="center">
|
||||
@@ -150,10 +192,11 @@ export const GameAnalyticsPanel = ({
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
||||
{recommendedMarketingPlan &&
|
||||
gameFeaturings &&
|
||||
fetchGameFeaturings && (
|
||||
<Grid item xs={12} sm={5} md={4}>
|
||||
<Grid item xs={12} sm={4} md={4}>
|
||||
<MarketingPlanSingleDisplay
|
||||
fetchGameFeaturings={fetchGameFeaturings}
|
||||
gameFeaturings={gameFeaturings}
|
||||
|
@@ -11,7 +11,6 @@ import Window from '../../Utils/Window';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import PlaceholderError from '../../UI/PlaceholderError';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import CreditOutDialog from './CashOutDialog';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
@@ -49,7 +48,6 @@ const UserEarningsWidget = ({ size }: Props) => {
|
||||
|
||||
const [earningsInMilliUsd, setEarningsInMilliUsd] = React.useState(0);
|
||||
const [earningsInCredits, setEarningsInCredits] = React.useState(0);
|
||||
const [error, setError] = React.useState(null);
|
||||
const intervalValuesUpdate = React.useRef(null);
|
||||
|
||||
const [selectedCashOutType, setSelectedCashOutType] = React.useState<
|
||||
@@ -58,43 +56,43 @@ const UserEarningsWidget = ({ size }: Props) => {
|
||||
|
||||
const animateEarnings = React.useCallback(
|
||||
async () => {
|
||||
if (!userEarningsBalance) return;
|
||||
|
||||
try {
|
||||
// Create an animation to show the earnings increasing.
|
||||
const targetMilliUsd = userEarningsBalance.amountInMilliUSDs;
|
||||
const targetCredits = userEarningsBalance.amountInCredits;
|
||||
|
||||
const duration = 500;
|
||||
const steps = 30;
|
||||
const intervalTime = duration / steps;
|
||||
|
||||
const milliUsdIncrement = targetMilliUsd / steps;
|
||||
const creditsIncrement = targetCredits / steps;
|
||||
|
||||
let currentMilliUsd = 0;
|
||||
let currentCredits = 0;
|
||||
let step = 0;
|
||||
|
||||
intervalValuesUpdate.current = setInterval(() => {
|
||||
step++;
|
||||
currentMilliUsd += milliUsdIncrement;
|
||||
currentCredits += creditsIncrement;
|
||||
|
||||
setEarningsInMilliUsd(currentMilliUsd);
|
||||
setEarningsInCredits(currentCredits);
|
||||
|
||||
if (step >= steps) {
|
||||
clearInterval(intervalValuesUpdate.current);
|
||||
// Ensure final values are exactly the target values
|
||||
setEarningsInMilliUsd(targetMilliUsd);
|
||||
setEarningsInCredits(targetCredits);
|
||||
}
|
||||
}, intervalTime);
|
||||
} catch (error) {
|
||||
console.error('Unable to get user earnings balance:', error);
|
||||
setError(error);
|
||||
if (!userEarningsBalance) {
|
||||
// In case the user logs out, reset the earnings.
|
||||
setEarningsInMilliUsd(0);
|
||||
setEarningsInCredits(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an animation to show the earnings increasing.
|
||||
const targetMilliUsd = userEarningsBalance.amountInMilliUSDs;
|
||||
const targetCredits = userEarningsBalance.amountInCredits;
|
||||
|
||||
const duration = 500;
|
||||
const steps = 30;
|
||||
const intervalTime = duration / steps;
|
||||
|
||||
const milliUsdIncrement = targetMilliUsd / steps;
|
||||
const creditsIncrement = targetCredits / steps;
|
||||
|
||||
let currentMilliUsd = 0;
|
||||
let currentCredits = 0;
|
||||
let step = 0;
|
||||
|
||||
intervalValuesUpdate.current = setInterval(() => {
|
||||
step++;
|
||||
currentMilliUsd += milliUsdIncrement;
|
||||
currentCredits += creditsIncrement;
|
||||
|
||||
setEarningsInMilliUsd(currentMilliUsd);
|
||||
setEarningsInCredits(currentCredits);
|
||||
|
||||
if (step >= steps) {
|
||||
clearInterval(intervalValuesUpdate.current);
|
||||
// Ensure final values are exactly the target values
|
||||
setEarningsInMilliUsd(targetMilliUsd);
|
||||
setEarningsInCredits(targetCredits);
|
||||
}
|
||||
}, intervalTime);
|
||||
},
|
||||
[userEarningsBalance]
|
||||
);
|
||||
@@ -127,16 +125,7 @@ const UserEarningsWidget = ({ size }: Props) => {
|
||||
userEarningsBalance &&
|
||||
earningsInMilliUsd >= userEarningsBalance.minAmountToCashoutInMilliUSDs;
|
||||
|
||||
const content = error ? (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<PlaceholderError onRetry={onRefreshEarningsBalance}>
|
||||
<Trans>
|
||||
Can't load your game earnings. Verify your internet connection or try
|
||||
again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
const content = (
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
|
@@ -10,17 +10,19 @@ import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import Text from '../../UI/Text';
|
||||
import { type Game } from '../../Utils/GDevelopServices/Game';
|
||||
import { SessionsChart } from '../GameAnalyticsCharts';
|
||||
import { GameAdEarningsChart, SessionsChart } from '../GameAnalyticsCharts';
|
||||
import { type GameMetrics } from '../../Utils/GDevelopServices/Analytics';
|
||||
import { buildLastWeekChartData } from '../GameAnalyticsEvaluator';
|
||||
import { buildChartData } from '../GameAnalyticsEvaluator';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import MarketingPlansDialog from '../../MarketingPlans/MarketingPlansDialog';
|
||||
import GameLinkAndShareIcons from '../GameLinkAndShareIcons';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { type GameAdEarning } from '../../Utils/GDevelopServices/Usage';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
import Link from '../../UI/Link';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
|
||||
const publishingHelpLink = getHelpLink('/publishing', 'publish-your-game');
|
||||
|
||||
@@ -30,29 +32,29 @@ type Props = {|
|
||||
game: Game,
|
||||
onSeeAll: () => void,
|
||||
gameMetrics: ?Array<GameMetrics>,
|
||||
gameAdEarnings: ?(GameAdEarning[]),
|
||||
gameUrl: ?string,
|
||||
|};
|
||||
|
||||
const AnalyticsWidget = ({ game, onSeeAll, gameMetrics, gameUrl }: Props) => {
|
||||
const AnalyticsWidget = ({
|
||||
game,
|
||||
onSeeAll,
|
||||
gameMetrics,
|
||||
gameAdEarnings,
|
||||
gameUrl,
|
||||
}: Props) => {
|
||||
const hasNoSession = gameMetrics && gameMetrics.length === 0;
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const oneWeekAgoIsoDate = new Date(
|
||||
new Date().setHours(0, 0, 0, 0) - 7 * 24 * 3600 * 1000
|
||||
).toISOString();
|
||||
const [
|
||||
marketingPlansDialogOpen,
|
||||
setMarketingPlansDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const { usages } = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const chartData = React.useMemo(
|
||||
() => {
|
||||
const lastWeekGameMetrics = gameMetrics
|
||||
? gameMetrics.filter(metrics => metrics.date > oneWeekAgoIsoDate)
|
||||
: null;
|
||||
|
||||
return buildLastWeekChartData(lastWeekGameMetrics);
|
||||
},
|
||||
[gameMetrics, oneWeekAgoIsoDate]
|
||||
const { weekChartData } = React.useMemo(
|
||||
() =>
|
||||
buildChartData({ gameMetrics, gameAdEarnings, usages, gameId: game.id }),
|
||||
[gameMetrics, gameAdEarnings, usages, game.id]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -74,69 +76,89 @@ const AnalyticsWidget = ({ game, onSeeAll, gameMetrics, gameUrl }: Props) => {
|
||||
widgetName="analytics"
|
||||
>
|
||||
<ResponsiveLineStackLayout expand noColumnMargin noMargin>
|
||||
{!gameMetrics ? (
|
||||
<div style={styles.loadingSpace} />
|
||||
) : hasNoSession ? (
|
||||
gameUrl ? (
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
alignItems={isMobile ? 'stretch' : 'flex-start'}
|
||||
noOverflowParent
|
||||
>
|
||||
<Spacer />
|
||||
<Text noMargin color="secondary">
|
||||
<Trans>
|
||||
No data to show yet. Share your game creator profile
|
||||
with more people to get more players!
|
||||
</Trans>
|
||||
</Text>
|
||||
<GameLinkAndShareIcons display="column" url={gameUrl} />
|
||||
</ColumnStackLayout>
|
||||
<Column expand noMargin>
|
||||
<Line alignItems="center">
|
||||
<Text size="sub-title">
|
||||
<Trans>Ads earnings</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
{!gameAdEarnings ? (
|
||||
<div style={styles.loadingSpace} />
|
||||
) : (
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
expand
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Spacer />
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
<Link
|
||||
href={publishingHelpLink}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(publishingHelpLink)
|
||||
}
|
||||
>
|
||||
Share your game
|
||||
</Link>{' '}
|
||||
and start collecting data from your players to better
|
||||
understand them.
|
||||
</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : (
|
||||
<Column expand noMargin>
|
||||
<Line alignItems="center" justifyContent="space-between">
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Sessions</Trans>
|
||||
</Text>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Coin fontSize="small" />}
|
||||
label={<Trans>Get more players</Trans>}
|
||||
onClick={() => setMarketingPlansDialogOpen(true)}
|
||||
<Column noMargin>
|
||||
<GameAdEarningsChart
|
||||
i18n={i18n}
|
||||
height={200}
|
||||
chartData={weekChartData}
|
||||
fontSize="small"
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
<Column expand noMargin>
|
||||
<Line alignItems="center" justifyContent="space-between">
|
||||
<Text size="sub-title">
|
||||
<Trans>Sessions</Trans>
|
||||
</Text>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Coin fontSize="small" />}
|
||||
label={<Trans>Get more players</Trans>}
|
||||
onClick={() => setMarketingPlansDialogOpen(true)}
|
||||
disabled={!gameMetrics}
|
||||
/>
|
||||
</Line>
|
||||
{!gameMetrics ? (
|
||||
<div style={styles.loadingSpace} />
|
||||
) : hasNoSession ? (
|
||||
gameUrl ? (
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
alignItems={isMobile ? 'stretch' : 'flex-start'}
|
||||
noOverflowParent
|
||||
>
|
||||
<Spacer />
|
||||
<Text noMargin color="secondary">
|
||||
<Trans>
|
||||
No data to show yet. Share your game creator profile
|
||||
with more people to get more players!
|
||||
</Trans>
|
||||
</Text>
|
||||
<GameLinkAndShareIcons display="column" url={gameUrl} />
|
||||
</ColumnStackLayout>
|
||||
) : (
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
expand
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Spacer />
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
<Link
|
||||
href={publishingHelpLink}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(publishingHelpLink)
|
||||
}
|
||||
>
|
||||
Share your game
|
||||
</Link>{' '}
|
||||
and start collecting data from your players to better
|
||||
understand them.
|
||||
</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : (
|
||||
<SessionsChart
|
||||
i18n={i18n}
|
||||
height={200}
|
||||
chartData={chartData}
|
||||
chartData={weekChartData}
|
||||
fontSize="small"
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
)}
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</DashboardWidget>
|
||||
)}
|
||||
|
@@ -39,7 +39,7 @@ const ServicesWidget = ({
|
||||
);
|
||||
return (
|
||||
<DashboardWidget
|
||||
widgetSize={'full'}
|
||||
widgetSize="full"
|
||||
title={<Trans>Player services</Trans>}
|
||||
widgetName="services"
|
||||
>
|
||||
|
@@ -60,6 +60,10 @@ import ProjectsWidget from './Widgets/ProjectsWidget';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { formatISO, subDays } from 'date-fns';
|
||||
import { daysShownForYear } from './GameAnalyticsEvaluator';
|
||||
import {
|
||||
getGameAdEarnings,
|
||||
type GameAdEarning,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
const styles = {
|
||||
mobileFooter: {
|
||||
@@ -140,9 +144,7 @@ const GameDashboard = ({
|
||||
const [feedbacks, setFeedbacks] = React.useState<?Array<Comment>>(null);
|
||||
const [builds, setBuilds] = React.useState<?Array<Build>>(null);
|
||||
const [publicGame, setPublicGame] = React.useState<?PublicGame>(null);
|
||||
const [gameRollingMetrics, setGameMetrics] = React.useState<?(GameMetrics[])>(
|
||||
null
|
||||
);
|
||||
const [gameMetrics, setGameMetrics] = React.useState<?(GameMetrics[])>(null);
|
||||
const [
|
||||
recommendedMarketingPlan,
|
||||
setRecommendedMarketingPlan,
|
||||
@@ -154,9 +156,10 @@ const GameDashboard = ({
|
||||
const [leaderboards, setLeaderboards] = React.useState<?Array<Leaderboard>>(
|
||||
null
|
||||
);
|
||||
const lastYearIsoDate = formatISO(subDays(new Date(), daysShownForYear), {
|
||||
representation: 'date',
|
||||
});
|
||||
const [
|
||||
gameAdEarnings,
|
||||
setGameAdEarnings,
|
||||
] = React.useState<?Array<GameAdEarning>>(null);
|
||||
|
||||
const webBuilds = builds
|
||||
? builds.filter(build => build.type === 'web-build')
|
||||
@@ -457,13 +460,25 @@ const GameDashboard = ({
|
||||
setLeaderboards(null);
|
||||
setGameFeaturings(null);
|
||||
setRecommendedMarketingPlan(null);
|
||||
setGameAdEarnings(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastYearIsoDate = formatISO(subDays(new Date(), daysShownForYear), {
|
||||
representation: 'date',
|
||||
});
|
||||
const gameCreatioDateIsoDate = formatISO(new Date(game.createdAt), {
|
||||
representation: 'date',
|
||||
});
|
||||
const todayIsoDate = formatISO(new Date(), {
|
||||
representation: 'date',
|
||||
});
|
||||
|
||||
const [
|
||||
feedbacks,
|
||||
builds,
|
||||
gameRollingMetrics,
|
||||
gameMetrics,
|
||||
gameAdEarnings,
|
||||
leaderboards,
|
||||
recommendedMarketingPlan,
|
||||
] = await Promise.all([
|
||||
@@ -478,6 +493,11 @@ const GameDashboard = ({
|
||||
game.id,
|
||||
lastYearIsoDate
|
||||
),
|
||||
getGameAdEarnings(getAuthorizationHeader, profile.id, {
|
||||
gameId: game.id,
|
||||
startIsoDate: gameCreatioDateIsoDate,
|
||||
endIsoDate: todayIsoDate,
|
||||
}),
|
||||
listGameActiveLeaderboards(getAuthorizationHeader, profile.id, game.id),
|
||||
getRecommendedMarketingPlan(getAuthorizationHeader, {
|
||||
gameId: game.id,
|
||||
@@ -487,16 +507,17 @@ const GameDashboard = ({
|
||||
]);
|
||||
setFeedbacks(feedbacks);
|
||||
setBuilds(builds);
|
||||
setGameMetrics(gameRollingMetrics);
|
||||
setGameMetrics(gameMetrics);
|
||||
setLeaderboards(leaderboards);
|
||||
setRecommendedMarketingPlan(recommendedMarketingPlan);
|
||||
setGameAdEarnings(gameAdEarnings);
|
||||
},
|
||||
[
|
||||
fetchGameFeaturings,
|
||||
game.id,
|
||||
getAuthorizationHeader,
|
||||
profile,
|
||||
lastYearIsoDate,
|
||||
game.createdAt,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -582,7 +603,8 @@ const GameDashboard = ({
|
||||
<Grid container spacing={2} ref={grid}>
|
||||
<AnalyticsWidget
|
||||
onSeeAll={() => setCurrentView('analytics')}
|
||||
gameMetrics={gameRollingMetrics}
|
||||
gameMetrics={gameMetrics}
|
||||
gameAdEarnings={gameAdEarnings}
|
||||
game={game}
|
||||
gameUrl={gameUrl}
|
||||
/>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import axios from 'axios';
|
||||
import { GDevelopAnalyticsApi } from './ApiConfigs';
|
||||
|
||||
export type GameMetrics = {
|
||||
export type GameMetrics = {|
|
||||
date: string,
|
||||
|
||||
sessions: ?{
|
||||
@@ -43,7 +43,7 @@ export type GameMetrics = {
|
||||
/** Day 7 retained players (number of players who played this day, and were new players 7 days earlier). */
|
||||
d7RetainedPlayers: number,
|
||||
},
|
||||
};
|
||||
|};
|
||||
|
||||
export const client = axios.create({
|
||||
baseURL: GDevelopAnalyticsApi.baseUrl,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import Window from '../Window';
|
||||
|
||||
const isDev = Window.isDev();
|
||||
const isDev = false;
|
||||
|
||||
export const GDevelopGamePreviews = {
|
||||
baseUrl: `https://game-previews.gdevelop.io/`,
|
||||
|
@@ -11,6 +11,8 @@ export type Usage = {
|
||||
userId: string,
|
||||
type: string,
|
||||
createdAt: number,
|
||||
description?: string,
|
||||
creditsPaid?: number,
|
||||
};
|
||||
export type Usages = Array<Usage>;
|
||||
|
||||
@@ -192,6 +194,14 @@ export type SubscriptionPlanWithPricingSystems = {|
|
||||
pricingSystems: SubscriptionPlanPricingSystem[],
|
||||
|};
|
||||
|
||||
export type GameAdEarning = {|
|
||||
gameId: string,
|
||||
date: string,
|
||||
adEarningsInMilliUSDs: number,
|
||||
adEarningsInCredits: number,
|
||||
updatedAt: number,
|
||||
|};
|
||||
|
||||
export interface UserEarningsBalance {
|
||||
userId: string;
|
||||
amountInMilliUSDs: number;
|
||||
@@ -313,6 +323,40 @@ export const getUserUsages = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getGameAdEarnings = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string,
|
||||
{
|
||||
gameId,
|
||||
startIsoDate,
|
||||
endIsoDate,
|
||||
}: {|
|
||||
gameId: string,
|
||||
startIsoDate: string,
|
||||
endIsoDate: string,
|
||||
|}
|
||||
): Promise<GameAdEarning[]> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
|
||||
const response = await apiClient.get(`/game-ad-earning`, {
|
||||
params: {
|
||||
userId,
|
||||
gameId,
|
||||
startIsoDate,
|
||||
endIsoDate,
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
});
|
||||
const gameAdEarnings = response.data;
|
||||
if (!Array.isArray(gameAdEarnings)) {
|
||||
throw new Error('Invalid response from the game ad earnings API');
|
||||
}
|
||||
|
||||
return gameAdEarnings;
|
||||
};
|
||||
|
||||
export const getUserEarningsBalance = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
|
Reference in New Issue
Block a user