give global properties __ prefix to easier find them
This commit is contained in:
@@ -42,7 +42,7 @@ function parsePath(path?: string): {
|
||||
return {
|
||||
query: parseSearchParams(url.searchParams),
|
||||
path: url.pathname,
|
||||
hash: url.hash ?? undefined,
|
||||
hash: url.hash || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -69,15 +69,18 @@ export async function postEvent(
|
||||
reply: FastifyReply
|
||||
) {
|
||||
let deviceId: string | null = null;
|
||||
const projectId = request.projectId;
|
||||
const body = request.body;
|
||||
const { projectId, body } = request;
|
||||
const properties = body.properties ?? {};
|
||||
const getProperty = (name: string): string | undefined => {
|
||||
return (properties[name] as string | null | undefined) ?? undefined;
|
||||
};
|
||||
const profileId = body.profileId ?? '';
|
||||
const createdAt = new Date(body.timestamp);
|
||||
const url = body.properties?.path;
|
||||
const url = getProperty('__path');
|
||||
const { path, hash, query } = parsePath(url);
|
||||
const referrer = isSameDomain(body.properties?.referrer, url)
|
||||
const referrer = isSameDomain(getProperty('__referrer'), url)
|
||||
? null
|
||||
: parseReferrer(body.properties?.referrer);
|
||||
: parseReferrer(getProperty('__referrer'));
|
||||
const utmReferrer = getReferrerWithQuery(query);
|
||||
const ip = getClientIp(request)!;
|
||||
const origin = request.headers.origin!;
|
||||
@@ -112,7 +115,14 @@ export async function postEvent(
|
||||
sessionId: event?.sessionId || '',
|
||||
profileId,
|
||||
projectId,
|
||||
properties: body.properties ?? {},
|
||||
properties: Object.assign(
|
||||
{},
|
||||
omit(['__path', '__referrer'], properties),
|
||||
{
|
||||
hash,
|
||||
query,
|
||||
}
|
||||
),
|
||||
createdAt,
|
||||
country: event?.country ?? '',
|
||||
city: event?.city ?? '',
|
||||
@@ -205,7 +215,7 @@ export async function postEvent(
|
||||
profileId,
|
||||
projectId,
|
||||
sessionId: createSessionStart ? uuid() : sessionStartEvent?.sessionId ?? '',
|
||||
properties: Object.assign({}, omit(['path', 'referrer'], body.properties), {
|
||||
properties: Object.assign({}, omit(['__path', '__referrer'], properties), {
|
||||
hash,
|
||||
query,
|
||||
}),
|
||||
|
||||
@@ -191,10 +191,6 @@ export async function createEvent(
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.properties.hash === '') {
|
||||
delete payload.properties.hash;
|
||||
}
|
||||
|
||||
const event: IClickhouseEvent = {
|
||||
id: uuid(),
|
||||
name: payload.name,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Constants from 'expo-constants';
|
||||
|
||||
import type { MixanOptions } from '@mixan/sdk';
|
||||
import { Mixan } from '@mixan/sdk';
|
||||
import type { PostEventPayload } from '@mixan/types';
|
||||
|
||||
type MixanNativeOptions = MixanOptions;
|
||||
|
||||
@@ -24,19 +25,22 @@ export class MixanNative extends Mixan<MixanNativeOptions> {
|
||||
|
||||
private async setProperties() {
|
||||
this.setGlobalProperties({
|
||||
version: Application.nativeApplicationVersion,
|
||||
buildNumber: Application.nativeBuildVersion,
|
||||
referrer:
|
||||
__version: Application.nativeApplicationVersion,
|
||||
__buildNumber: Application.nativeBuildVersion,
|
||||
__referrer:
|
||||
Platform.OS === 'android'
|
||||
? await Application.getInstallReferrerAsync()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public screenView(route: string, properties?: Record<string, unknown>): void {
|
||||
public screenView(
|
||||
route: string,
|
||||
properties?: PostEventPayload['properties']
|
||||
): void {
|
||||
super.event('screen_view', {
|
||||
...properties,
|
||||
path: route,
|
||||
__path: route,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import Script from 'next/script';
|
||||
|
||||
import type { MixanEventOptions } from '@mixan/sdk';
|
||||
import type { MixanWebOptions } from '@mixan/sdk-web';
|
||||
import type { UpdateProfilePayload } from '@mixan/types';
|
||||
import type {
|
||||
MixanEventOptions,
|
||||
PostEventPayload,
|
||||
UpdateProfilePayload,
|
||||
} from '@mixan/types';
|
||||
|
||||
const CDN_URL = 'http://localhost:3002/op.js';
|
||||
|
||||
@@ -73,11 +76,14 @@ export function SetProfileId({ value }: SetProfileIdProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function trackEvent(name: string, data?: Record<string, unknown>) {
|
||||
export function trackEvent(
|
||||
name: string,
|
||||
data?: PostEventPayload['properties']
|
||||
) {
|
||||
window.op('event', name, data);
|
||||
}
|
||||
|
||||
export function trackScreenView(data?: Record<string, unknown>) {
|
||||
export function trackScreenView(data?: PostEventPayload['properties']) {
|
||||
trackEvent('screen_view', data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MixanOptions } from '@mixan/sdk';
|
||||
import { Mixan } from '@mixan/sdk';
|
||||
import type { PostEventPayload } from '@mixan/types';
|
||||
|
||||
export type MixanWebOptions = MixanOptions & {
|
||||
trackOutgoingLinks?: boolean;
|
||||
@@ -22,7 +23,7 @@ export class MixanWeb extends Mixan<MixanWebOptions> {
|
||||
|
||||
if (!this.isServer()) {
|
||||
this.setGlobalProperties({
|
||||
referrer: document.referrer,
|
||||
__referrer: document.referrer,
|
||||
});
|
||||
|
||||
if (this.options.trackOutgoingLinks) {
|
||||
@@ -134,7 +135,7 @@ export class MixanWeb extends Mixan<MixanWebOptions> {
|
||||
});
|
||||
}
|
||||
|
||||
public screenView(properties?: Record<string, unknown>): void {
|
||||
public screenView(properties?: PostEventPayload['properties']): void {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
@@ -148,8 +149,8 @@ export class MixanWeb extends Mixan<MixanWebOptions> {
|
||||
this.lastPath = path;
|
||||
super.event('screen_view', {
|
||||
...(properties ?? {}),
|
||||
path,
|
||||
title: document.title,
|
||||
__path: path,
|
||||
__title: document.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DecrementProfilePayload,
|
||||
IncrementProfilePayload,
|
||||
MixanEventOptions,
|
||||
PostEventPayload,
|
||||
UpdateProfilePayload,
|
||||
} from '@mixan/types';
|
||||
@@ -21,10 +22,6 @@ export interface MixanState {
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MixanEventOptions {
|
||||
profileId?: string;
|
||||
}
|
||||
|
||||
function awaitProperties(
|
||||
properties: Record<string, string | Promise<string | null>>
|
||||
): Promise<Record<string, string>> {
|
||||
@@ -168,10 +165,7 @@ export class Mixan<Options extends MixanOptions = MixanOptions> {
|
||||
});
|
||||
}
|
||||
|
||||
public event(
|
||||
name: string,
|
||||
properties?: Record<string, unknown> & MixanEventOptions
|
||||
) {
|
||||
public event(name: string, properties?: PostEventPayload['properties']) {
|
||||
const profileId = properties?.profileId ?? this.state.profileId;
|
||||
delete properties?.profileId;
|
||||
this.api
|
||||
@@ -200,7 +194,6 @@ export class Mixan<Options extends MixanOptions = MixanOptions> {
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.state.properties = {};
|
||||
this.state.deviceId = undefined;
|
||||
this.state.profileId = undefined;
|
||||
if (this.options.removeDeviceId) {
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import type {
|
||||
EventPayload,
|
||||
MixanErrorResponse,
|
||||
ProfilePayload,
|
||||
} from '@mixan/types';
|
||||
|
||||
export interface NewMixanOptions {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
batchInterval?: number;
|
||||
maxBatchSize?: number;
|
||||
sessionTimeout?: number;
|
||||
session?: boolean;
|
||||
verbose?: boolean;
|
||||
profile?: boolean;
|
||||
trackIp?: boolean;
|
||||
setItem: (key: string, profileId: string) => void;
|
||||
getItem: (key: string) => string | null;
|
||||
removeItem: (key: string) => void;
|
||||
}
|
||||
export type MixanOptions = Required<NewMixanOptions>;
|
||||
|
||||
class Fetcher {
|
||||
private url: string;
|
||||
private clientId: string;
|
||||
private clientSecret: string;
|
||||
private logger: (...args: any[]) => void;
|
||||
|
||||
constructor(options: MixanOptions) {
|
||||
this.url = options.url;
|
||||
this.clientId = options.clientId;
|
||||
this.clientSecret = options.clientSecret;
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
}
|
||||
|
||||
post<PostData, PostResponse>(
|
||||
path: string,
|
||||
data?: PostData,
|
||||
options?: RequestInit
|
||||
): Promise<PostResponse | null> {
|
||||
const url = `${this.url}${path}`;
|
||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2));
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
['mixan-client-id']: this.clientId,
|
||||
['mixan-client-secret']: this.clientSecret,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data ?? {}),
|
||||
keepalive: true,
|
||||
...(options ?? {}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const response = (await res.json()) as
|
||||
| MixanErrorResponse
|
||||
| PostResponse;
|
||||
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof response === 'object' &&
|
||||
'status' in response &&
|
||||
response.status === 'error'
|
||||
) {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`,
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response as PostResponse;
|
||||
})
|
||||
.catch(() => {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Batcher<T> {
|
||||
queue: T[] = [];
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
callback: (queue: T[]) => void;
|
||||
maxBatchSize: number;
|
||||
batchInterval: number;
|
||||
|
||||
constructor(options: MixanOptions, callback: (queue: T[]) => void) {
|
||||
this.callback = callback;
|
||||
this.maxBatchSize = options.maxBatchSize;
|
||||
this.batchInterval = options.batchInterval;
|
||||
}
|
||||
|
||||
add(payload: T) {
|
||||
this.queue.push(payload);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queue.length >= this.maxBatchSize) {
|
||||
this.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer = setTimeout(this.send.bind(this), this.batchInterval);
|
||||
}
|
||||
|
||||
send() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length > 0) {
|
||||
this.callback(this.queue);
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Mixan {
|
||||
private fetch: Fetcher;
|
||||
private eventBatcher: Batcher<EventPayload>;
|
||||
private profileId?: string;
|
||||
private options: MixanOptions;
|
||||
private logger: (...args: any[]) => void;
|
||||
private globalProperties: Record<string, unknown> = {};
|
||||
private lastEventAt: string;
|
||||
private promiseIp: Promise<string | null>;
|
||||
|
||||
constructor(options: NewMixanOptions) {
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
this.options = {
|
||||
sessionTimeout: 1000 * 60 * 30,
|
||||
session: true,
|
||||
verbose: false,
|
||||
batchInterval: 10000,
|
||||
maxBatchSize: 10,
|
||||
trackIp: false,
|
||||
profile: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.lastEventAt =
|
||||
this.options.getItem('@mixan:lastEventAt') ?? '1970-01-01';
|
||||
this.fetch = new Fetcher(this.options);
|
||||
this.eventBatcher = new Batcher(this.options, (queue) => {
|
||||
this.fetch.post(
|
||||
'/events',
|
||||
queue.map((item) => ({
|
||||
...item,
|
||||
properties: {
|
||||
...this.globalProperties,
|
||||
...item.properties,
|
||||
},
|
||||
profileId: item.profileId ?? this.profileId ?? null,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
this.promiseIp = this.options.trackIp
|
||||
? fetch('https://api.ipify.org')
|
||||
.then((res) => res.text())
|
||||
.catch(() => null)
|
||||
: Promise.resolve(null);
|
||||
}
|
||||
|
||||
async ip() {
|
||||
return this.promiseIp;
|
||||
}
|
||||
|
||||
timestamp(modify = 0) {
|
||||
return new Date(Date.now() + modify).toISOString();
|
||||
}
|
||||
|
||||
init(properties?: Record<string, unknown>) {
|
||||
if (properties) {
|
||||
this.setGlobalProperties(properties);
|
||||
}
|
||||
this.logger('Mixan: Init');
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
|
||||
event(name: string, properties: Record<string, unknown> = {}) {
|
||||
const now = new Date();
|
||||
const isSessionStart =
|
||||
this.options.session &&
|
||||
now.getTime() - new Date(this.lastEventAt).getTime() >
|
||||
this.options.sessionTimeout;
|
||||
|
||||
if (isSessionStart) {
|
||||
this.logger('Mixan: Session start');
|
||||
this.eventBatcher.add({
|
||||
name: 'session_start',
|
||||
time: this.timestamp(-10),
|
||||
properties: {},
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger('Mixan: Queue event', name);
|
||||
this.eventBatcher.add({
|
||||
name,
|
||||
properties,
|
||||
time: this.timestamp(),
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
this.lastEventAt = this.timestamp();
|
||||
this.options.setItem('@mixan:lastEventAt', this.lastEventAt);
|
||||
}
|
||||
|
||||
private async setAnonymousUser(retryCount = 0) {
|
||||
if (!this.options.profile) {
|
||||
return;
|
||||
}
|
||||
const profileId = this.options.getItem('@mixan:profileId');
|
||||
if (profileId) {
|
||||
this.profileId = profileId;
|
||||
await this.setUser({
|
||||
properties: this.globalProperties,
|
||||
});
|
||||
this.logger('Mixan: Use existing profile', this.profileId);
|
||||
} else {
|
||||
const res = await this.fetch.post<ProfilePayload, { id: string }>(
|
||||
'/profiles',
|
||||
{
|
||||
properties: this.globalProperties,
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
this.profileId = res.id;
|
||||
this.options.setItem('@mixan:profileId', res.id);
|
||||
this.logger('Mixan: Create new profile', this.profileId);
|
||||
} else if (retryCount < 2) {
|
||||
setTimeout(() => {
|
||||
this.setAnonymousUser(retryCount + 1);
|
||||
}, 500);
|
||||
} else {
|
||||
this.logger('Mixan: Failed to create new profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setUser(profile: ProfilePayload) {
|
||||
if (!this.options.profile) {
|
||||
return;
|
||||
}
|
||||
if (!this.profileId) {
|
||||
return this.logger('Mixan: Set user failed, no profileId');
|
||||
}
|
||||
this.logger('Mixan: Set user', profile);
|
||||
await this.fetch.post(`/profiles/${this.profileId}`, profile, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
async setUserProperty(
|
||||
name: string,
|
||||
value: string | number | boolean | Record<string, unknown> | unknown[]
|
||||
) {
|
||||
if (!this.options.profile) {
|
||||
return;
|
||||
}
|
||||
if (!this.profileId) {
|
||||
return this.logger('Mixan: Set user property, no profileId');
|
||||
}
|
||||
this.logger('Mixan: Set user property', name, value);
|
||||
await this.fetch.post(`/profiles/${this.profileId}`, {
|
||||
properties: {
|
||||
[name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setGlobalProperties(properties: Record<string, unknown>) {
|
||||
if (typeof properties !== 'object') {
|
||||
return this.logger(
|
||||
'Mixan: Set global properties failed, properties must be an object'
|
||||
);
|
||||
}
|
||||
this.logger('Mixan: Set global properties', properties);
|
||||
this.globalProperties = {
|
||||
...this.globalProperties,
|
||||
...properties,
|
||||
};
|
||||
}
|
||||
|
||||
async increment(name: string, value = 1) {
|
||||
if (!this.options.profile) {
|
||||
return;
|
||||
}
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Increment failed, no profileId');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger('Mixan: Increment user property', name, value);
|
||||
await this.fetch.post(
|
||||
`/profiles/${this.profileId}/increment`,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async decrement(name: string, value = 1) {
|
||||
if (!this.options.profile) {
|
||||
return;
|
||||
}
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Decrement failed, no profileId');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger('Mixan: Decrement user property', name, value);
|
||||
await this.fetch.post(
|
||||
`/profiles/${this.profileId}/decrement`,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.logger('Mixan: Flushing events queue');
|
||||
this.eventBatcher.send();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logger('Mixan: Clear, send remaining events and remove profileId');
|
||||
this.eventBatcher.send();
|
||||
this.options.removeItem('@mixan:profileId');
|
||||
this.options.removeItem('@mixan:lastEventAt');
|
||||
this.profileId = undefined;
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
}
|
||||
@@ -134,16 +134,16 @@ export interface MixanResponse<T> {
|
||||
|
||||
// NEW
|
||||
|
||||
export interface MixanEventOptions {
|
||||
profileId?: string;
|
||||
}
|
||||
|
||||
export interface PostEventPayload {
|
||||
name: string;
|
||||
timestamp: string;
|
||||
deviceId?: string;
|
||||
profileId?: string;
|
||||
properties?: Record<string, unknown> & {
|
||||
title?: string | undefined;
|
||||
referrer?: string | undefined;
|
||||
path?: string | undefined;
|
||||
};
|
||||
properties?: Record<string, unknown> & MixanEventOptions;
|
||||
}
|
||||
|
||||
export interface UpdateProfilePayload {
|
||||
|
||||
Reference in New Issue
Block a user