feat: revenue tracking

* wip

* wip

* wip

* wip

* show revenue better on overview

* align realtime and overview counters

* update revenue docs

* always return device id

* add project settings, improve projects charts,

* fix: comments

* fixes

* fix migration

* ignore sql files

* fix comments
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-11-19 14:27:34 +01:00
committed by GitHub
parent d61cbf6f2c
commit 790801b728
58 changed files with 2191 additions and 23691 deletions

View File

@@ -14,14 +14,14 @@ import type { IServiceCreateEventPayload, IServiceEvent } from '@openpanel/db';
import {
checkNotificationRulesForEvent,
createEvent,
eventBuffer,
sessionBuffer,
} from '@openpanel/db';
import type { ILogger } from '@openpanel/logger';
import type { EventsQueuePayloadIncomingEvent } from '@openpanel/queue';
import * as R from 'ramda';
import { v4 as uuid } from 'uuid';
const GLOBAL_PROPERTIES = ['__path', '__referrer', '__timestamp'];
const GLOBAL_PROPERTIES = ['__path', '__referrer', '__timestamp', '__revenue'];
// This function will merge two objects.
// First it will strip '' and undefined/null from B
@@ -41,6 +41,17 @@ async function createEventAndNotify(
return event;
}
const parseRevenue = (revenue: unknown): number | undefined => {
if (!revenue) return undefined;
if (typeof revenue === 'number') return revenue;
if (typeof revenue === 'string') {
const parsed = Number.parseFloat(revenue);
if (Number.isNaN(parsed)) return undefined;
return parsed;
}
return undefined;
};
export async function incomingEvent(
jobPayload: EventsQueuePayloadIncomingEvent['payload'],
) {
@@ -115,12 +126,16 @@ export async function incomingEvent(
device: uaInfo.device,
brand: uaInfo.brand,
model: uaInfo.model,
revenue:
body.name === 'revenue' && '__revenue' in properties
? parseRevenue(properties.__revenue)
: undefined,
} as const;
// if timestamp is from the past we dont want to create a new session
if (uaInfo.isServer || isTimestampFromThePast) {
const screenView = profileId
? await eventBuffer.getLastScreenView({
const session = profileId
? await sessionBuffer.getExistingSession({
profileId,
projectId,
})
@@ -128,25 +143,25 @@ export async function incomingEvent(
const payload = {
...baseEvent,
deviceId: screenView?.deviceId ?? '',
sessionId: screenView?.sessionId ?? '',
referrer: screenView?.referrer ?? undefined,
referrerName: screenView?.referrerName ?? undefined,
referrerType: screenView?.referrerType ?? undefined,
path: screenView?.path ?? baseEvent.path,
os: screenView?.os ?? baseEvent.os,
osVersion: screenView?.osVersion ?? baseEvent.osVersion,
browserVersion: screenView?.browserVersion ?? baseEvent.browserVersion,
browser: screenView?.browser ?? baseEvent.browser,
device: screenView?.device ?? baseEvent.device,
brand: screenView?.brand ?? baseEvent.brand,
model: screenView?.model ?? baseEvent.model,
city: screenView?.city ?? baseEvent.city,
country: screenView?.country ?? baseEvent.country,
region: screenView?.region ?? baseEvent.region,
longitude: screenView?.longitude ?? baseEvent.longitude,
latitude: screenView?.latitude ?? baseEvent.latitude,
origin: screenView?.origin ?? baseEvent.origin,
deviceId: session?.device_id ?? '',
sessionId: session?.id ?? '',
referrer: session?.referrer ?? undefined,
referrerName: session?.referrer_name ?? undefined,
referrerType: session?.referrer_type ?? undefined,
path: session?.exit_path ?? baseEvent.path,
origin: session?.exit_origin ?? baseEvent.origin,
os: session?.os ?? baseEvent.os,
osVersion: session?.os_version ?? baseEvent.osVersion,
browserVersion: session?.browser_version ?? baseEvent.browserVersion,
browser: session?.browser ?? baseEvent.browser,
device: session?.device ?? baseEvent.device,
brand: session?.brand ?? baseEvent.brand,
model: session?.model ?? baseEvent.model,
city: session?.city ?? baseEvent.city,
country: session?.country ?? baseEvent.country,
region: session?.region ?? baseEvent.region,
longitude: session?.longitude ?? baseEvent.longitude,
latitude: session?.latitude ?? baseEvent.latitude,
};
return createEventAndNotify(payload as IServiceEvent, logger);
@@ -160,8 +175,7 @@ export async function incomingEvent(
});
const lastScreenView = sessionEnd
? await eventBuffer.getLastScreenView({
projectId,
? await sessionBuffer.getExistingSession({
sessionId: sessionEnd.sessionId,
})
: null;
@@ -173,8 +187,8 @@ export async function incomingEvent(
referrerName: sessionEnd?.referrerName ?? baseEvent.referrerName,
referrerType: sessionEnd?.referrerType ?? baseEvent.referrerType,
// if the path is not set, use the last screen view path
path: baseEvent.path || lastScreenView?.path || '',
origin: baseEvent.origin || lastScreenView?.origin || '',
path: baseEvent.path || lastScreenView?.exit_path || '',
origin: baseEvent.origin || lastScreenView?.exit_origin || '',
} as Partial<IServiceCreateEventPayload>) as IServiceCreateEventPayload;
if (!sessionEnd) {