Skip to content

Commit

Permalink
tags and revenue tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
ijkml committed Nov 24, 2024
1 parent 5ccf05d commit a89b448
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 28 deletions.
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default defineNuxtConfig({
excludeQueryParams: false,
trailingSlash: 'always',
proxy: 'cloak',
tag: 'gondor',
},

appConfig: {
Expand Down
2 changes: 2 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default defineNuxtModule<ModuleOptions>({

const envHost = ENV.NUXT_UMAMI_HOST || ENV.NUXT_PUBLIC_UMAMI_HOST;
const envId = ENV.NUXT_UMAMI_ID || ENV.NUXT_PUBLIC_UMAMI_ID;
const envTag = ENV.NUXT_UMAMI_TAG || ENV.NUXT_PUBLIC_UMAMI_TAG;

const {
enabled,
Expand All @@ -52,6 +53,7 @@ export default defineNuxtModule<ModuleOptions>({
...options,
...(isValidString(envId) && { id: envId }),
...(isValidString(envHost) && { host: envHost }),
...(isValidString(envTag) && { tag: envTag }),
});

const endpoint = host ? new URL(host).origin + (customEndpoint || '/api/send') : '';
Expand Down
53 changes: 49 additions & 4 deletions src/runtime/composables.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type {
StaticPayload, EventPayload, ViewPayload, IdentifyPayload,
PreflightResult, EventData, FetchResult,
CurrencyCode,
EventData,
EventPayload,
FetchResult,
IdentifyPayload,
PreflightResult,
StaticPayload,
ViewPayload,
} from '../types';
import { earlyPromise, flattenObject, isValidString } from './utils';
import { buildPathUrl, collect, config, logger } from '#build/umami.config.mjs';
import { earlyPromise, flattenObject, isValidString } from './utils';

let configChecks: PreflightResult | undefined;
let staticPayload: StaticPayload | undefined;
Expand Down Expand Up @@ -46,10 +52,13 @@ function getStaticPayload(): StaticPayload {
navigator: { language },
} = window;

const { tag } = config;

staticPayload = {
hostname,
language,
screen: `${width}x${height}`,
...(tag ? { tag } : null),
};

return staticPayload;
Expand Down Expand Up @@ -175,4 +184,40 @@ function umIdentify(sessionData?: EventData): FetchResult {
});
}

export { umTrackEvent, umTrackView, umIdentify };
/**
* Tracks financial performance
* @see [Umami Docs](https://umami.is/docs/reports/report-revenue)
*
* @param eventName [revenue] event name
* @param revenue revenue / amount
* @param currency currency code (defaults to USD)
* ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes))
*/
function umTrackRevenue(
eventName: string,
revenue: number,
currency: CurrencyCode = 'USD',
): FetchResult {
const $rev = typeof revenue === 'number' ? revenue : Number(revenue);

if (Number.isNaN($rev) || !Number.isFinite(revenue)) {
// if you ever run into troubles with isFinite (or not),
// please buy me a coffee ;) bmc.link/ijkml
logger('revenue', revenue);
return earlyPromise(false);
}

let $cur: string | null = null;

if (typeof currency === 'string' && /^[A-Z]{3}$/i.test(currency.trim()))
$cur = currency.trim();
else
logger('currency', `Got: ${currency}`);

return umTrackEvent(eventName, {
revenue: $rev,
...($cur ? { currency: $cur } : null),
});
}

export { umIdentify, umTrackEvent, umTrackRevenue, umTrackView };
9 changes: 6 additions & 3 deletions src/runtime/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { PreflightResult } from '../types';

type PreflightErrId = Exclude<PreflightResult, 'ssr' | true>
| 'collect' | 'directive' | 'event-name' | 'endpoint' | 'id' | 'enabled';
| 'collect' | 'directive' | 'event-name' | 'endpoint' | 'id'
| 'enabled' | 'currency' | 'revenue';
type LogLevel = 'info' | 'warn' | 'error';
interface ErrorObj {
level: LogLevel;
Expand All @@ -16,8 +17,10 @@ const warnings: Record<PreflightErrId, ErrorObj> = {
'localhost': { level: 'info', text: 'Tracking disabled on localhost' },
'local-storage': { level: 'info', text: 'Tracking disabled via local-storage' },
'collect': { level: 'error', text: 'Uhm... Something went wrong and I have no clue.' },
'directive': { level: 'error', text: 'Invalid v-umami directive value. Expected string or object with {key:value} pairs. See https://github.com/ijkml/nuxt-umami#available-methods' },
'directive': { level: 'error', text: 'Invalid v-umami directive value. Expected string or object with {key:value} pairs. See https://umami.nuxt.dev/api/usage#directive' },
'event-name': { level: 'warn', text: 'An Umami track event was fired without a name. `#unknown-event` will be used as event name.' },
'currency': { level: 'warn', text: 'Invalid currency passed. Expected ISO 4217 format. See https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes' },
'revenue': { level: 'error', text: 'Revenue is not a number. Expected number, got: ' },
};

function logger(id: PreflightErrId, raw?: unknown) {
Expand All @@ -30,4 +33,4 @@ function logger(id: PreflightErrId, raw?: unknown) {

function fauxLogger(..._args: Parameters<typeof logger>) {}

export { logger, fauxLogger };
export { fauxLogger, logger };
39 changes: 31 additions & 8 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type {
EventPayload, PayloadTypes, ServerPayload, ViewPayload,
FetchResult, ModuleOptions, NormalizedModuleOptions,
EventPayload,
FetchResult,
ModuleOptions,
NormalizedModuleOptions,
PayloadTypes,
ServerPayload,
ViewPayload,
} from '../types';

function earlyPromise(ok: boolean): FetchResult {
Expand Down Expand Up @@ -35,6 +40,7 @@ function normalizeConfig(options: ModuleOptions = {}): NormalizedModuleOptions {
logErrors = false,
enabled = true,
trailingSlash = 'any',
tag = undefined,
} = options;

return {
Expand Down Expand Up @@ -62,10 +68,12 @@ function normalizeConfig(options: ModuleOptions = {}): NormalizedModuleOptions {
if (
isValidString(trailingSlash)
&& ['always', 'never'].includes(trailingSlash.trim())
)
) {
return trailingSlash.trim() as typeof trailingSlash;
}
return 'any';
})(),
tag: isValidString(tag) ? tag.trim() : null,
ignoreLocalhost: ignoreLocalhost === true,
autoTrack: autoTrack !== false,
useDirective: useDirective === true,
Expand Down Expand Up @@ -117,6 +125,7 @@ const _payloadProps: Record<keyof Payload, PropertyValidator> = {
url: 'nonempty',
referrer: 'string',
title: 'string',
tag: 'skip', // optional property
name: 'skip', // optional, 'nonempty' in EventPayload
data: 'skip', // optional, 'data' in EventPayload & IdentifyPayload
} as const;
Expand All @@ -133,8 +142,12 @@ function isValidPayload(obj: object): obj is Payload {
const validators: typeof _payloadProps = { ..._payloadProps };

const validatorKeys: Array<keyof Payload> = [
'hostname', 'language', 'screen',
'url', 'referrer', 'title',
'hostname',
'language',
'screen',
'url',
'referrer',
'title',
];

if (objKeys.includes('name')) {
Expand All @@ -149,11 +162,19 @@ function isValidPayload(obj: object): obj is Payload {
validators.data = 'data';
}

// optional property is present, update validators
if (objKeys.includes('tag')) {
validatorKeys.push('tag');
validators.tag = 'string';
}

// check: all keys are present, no more, no less
if (
objKeys.length !== validatorKeys.length
|| !validatorKeys.every(k => objKeys.includes(k))
) return false;
) {
return false;
}

// run each value against its validator
for (const key in obj) {
Expand Down Expand Up @@ -185,7 +206,9 @@ function parseEventBody(body: unknown): ValidatePayloadReturn {
'type' in body && isValidString(body.type)
&& 'cache' in body && typeof body.cache === 'string'
&& 'payload' in body && isRecord(body.payload)
)) return error;
)) {
return error;
}

const { payload, cache, type } = body;

Expand All @@ -204,8 +227,8 @@ function parseEventBody(body: unknown): ValidatePayloadReturn {

export {
earlyPromise,
isValidString,
flattenObject,
isValidString,
normalizeConfig,
parseEventBody,
};
43 changes: 30 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ModuleOptions = Partial<{
*
* @required true
* @example 'https://ijkml.xyz/'
* @see [How to find?](https://umami.nuxt.dev/api/configuration#finding-config-options).
*/
host: string;
/**
Expand Down Expand Up @@ -46,9 +47,15 @@ type ModuleOptions = Partial<{
* Self-hosted Umami lets you set a COLLECT_API_ENDPOINT, which is:
* - `/api/collect` by default in Umami v1
* - `/api/send` by default in Umami v2.
* See Umami [Docs](https://umami.is/docs/environment-variables).
* See [Umami Docs](https://umami.is/docs/environment-variables).
*/
customEndpoint: string | null;
/**
* Use Umami tags for A/B testing or to group events.
*
* See [Umami Docs](https://umami.is/docs/tags).
*/
tag: string | null;
/**
* Exclude query/search params from tracked urls
*
Expand All @@ -72,7 +79,9 @@ type ModuleOptions = Partial<{
*/
logErrors: boolean;
/**
* API proxy mode (see docs)
* API proxy mode
*
* @see [Documentation](https://umami.nuxt.dev/api/configuration#proxy-mode).
*
* @default false
*/
Expand Down Expand Up @@ -116,6 +125,7 @@ interface StaticPayload {
screen: string;
language: string;
hostname: string;
tag?: string;
}

interface ViewPayload extends StaticPayload {
Expand Down Expand Up @@ -143,21 +153,28 @@ type FetchResult = Promise<{ ok: boolean }>;
type FetchFn = (load: ServerPayload) => FetchResult;
type BuildPathUrlFn = () => string;

type _Letter = `${'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'
| 'I' | 'J' | 'K' | 'M' | 'L' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S'
| 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'}`;

type CurrencyCode = Uppercase<`${_Letter}${_Letter}${_Letter}`>;

export type {
PreflightResult,
BuildPathUrlFn,
CurrencyCode,
EventData,
EventPayload,
FetchFn,
FetchResult,
IdentifyPayload,
ModuleMode,
ModuleOptions,
EventData,
StaticPayload,
NormalizedModuleOptions,
UmPublicConfig,
UmPrivateConfig,
ModuleMode,
FetchFn,
BuildPathUrlFn,
PayloadTypes,
ViewPayload,
EventPayload,
IdentifyPayload,
PreflightResult,
ServerPayload,
StaticPayload,
UmPrivateConfig,
UmPublicConfig,
ViewPayload,
};

0 comments on commit a89b448

Please sign in to comment.