fix: better validation of events + clean up (#267)
This commit is contained in:
@@ -38,11 +38,9 @@ COPY packages/redis/package.json packages/redis/
|
|||||||
COPY packages/logger/package.json packages/logger/
|
COPY packages/logger/package.json packages/logger/
|
||||||
COPY packages/common/package.json packages/common/
|
COPY packages/common/package.json packages/common/
|
||||||
COPY packages/payments/package.json packages/payments/
|
COPY packages/payments/package.json packages/payments/
|
||||||
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
|
|
||||||
COPY packages/constants/package.json packages/constants/
|
COPY packages/constants/package.json packages/constants/
|
||||||
COPY packages/validation/package.json packages/validation/
|
COPY packages/validation/package.json packages/validation/
|
||||||
COPY packages/integrations/package.json packages/integrations/
|
COPY packages/integrations/package.json packages/integrations/
|
||||||
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
|
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
# BUILD
|
# BUILD
|
||||||
@@ -107,7 +105,6 @@ COPY --from=build /app/packages/redis ./packages/redis
|
|||||||
COPY --from=build /app/packages/logger ./packages/logger
|
COPY --from=build /app/packages/logger ./packages/logger
|
||||||
COPY --from=build /app/packages/common ./packages/common
|
COPY --from=build /app/packages/common ./packages/common
|
||||||
COPY --from=build /app/packages/payments ./packages/payments
|
COPY --from=build /app/packages/payments ./packages/payments
|
||||||
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
|
|
||||||
COPY --from=build /app/packages/constants ./packages/constants
|
COPY --from=build /app/packages/constants ./packages/constants
|
||||||
COPY --from=build /app/packages/validation ./packages/validation
|
COPY --from=build /app/packages/validation ./packages/validation
|
||||||
COPY --from=build /app/packages/integrations ./packages/integrations
|
COPY --from=build /app/packages/integrations ./packages/integrations
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^9.0.1",
|
"@faker-js/faker": "^9.0.1",
|
||||||
"@openpanel/sdk": "workspace:*",
|
|
||||||
"@openpanel/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
|
|||||||
import { generateDeviceId, parseUserAgent } from '@openpanel/common/server';
|
import { generateDeviceId, parseUserAgent } from '@openpanel/common/server';
|
||||||
import { getSalts } from '@openpanel/db';
|
import { getSalts } from '@openpanel/db';
|
||||||
import { getEventsGroupQueueShard } from '@openpanel/queue';
|
import { getEventsGroupQueueShard } from '@openpanel/queue';
|
||||||
import type { PostEventPayload } from '@openpanel/sdk';
|
|
||||||
|
|
||||||
import { generateId, slug } from '@openpanel/common';
|
import { generateId, slug } from '@openpanel/common';
|
||||||
import { getGeoLocation } from '@openpanel/geo';
|
import { getGeoLocation } from '@openpanel/geo';
|
||||||
|
import type { DeprecatedPostEventPayload } from '@openpanel/validation';
|
||||||
import { getStringHeaders, getTimestamp } from './track.controller';
|
import { getStringHeaders, getTimestamp } from './track.controller';
|
||||||
|
|
||||||
export async function postEvent(
|
export async function postEvent(
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: PostEventPayload;
|
Body: DeprecatedPostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { parseUserAgent } from '@openpanel/common/server';
|
|||||||
import { getProfileById, upsertProfile } from '@openpanel/db';
|
import { getProfileById, upsertProfile } from '@openpanel/db';
|
||||||
import { getGeoLocation } from '@openpanel/geo';
|
import { getGeoLocation } from '@openpanel/geo';
|
||||||
import type {
|
import type {
|
||||||
IncrementProfilePayload,
|
DeprecatedIncrementProfilePayload,
|
||||||
UpdateProfilePayload,
|
DeprecatedUpdateProfilePayload,
|
||||||
} from '@openpanel/sdk';
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
export async function updateProfile(
|
export async function updateProfile(
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: UpdateProfilePayload;
|
Body: DeprecatedUpdateProfilePayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
@@ -52,7 +52,7 @@ export async function updateProfile(
|
|||||||
|
|
||||||
export async function incrementProfileProperty(
|
export async function incrementProfileProperty(
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: IncrementProfilePayload;
|
Body: DeprecatedIncrementProfilePayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
@@ -94,7 +94,7 @@ export async function incrementProfileProperty(
|
|||||||
|
|
||||||
export async function decrementProfileProperty(
|
export async function decrementProfileProperty(
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: IncrementProfilePayload;
|
Body: DeprecatedIncrementProfilePayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import { getProfileById, getSalts, upsertProfile } from '@openpanel/db';
|
|||||||
import { type GeoLocation, getGeoLocation } from '@openpanel/geo';
|
import { type GeoLocation, getGeoLocation } from '@openpanel/geo';
|
||||||
import { getEventsGroupQueueShard } from '@openpanel/queue';
|
import { getEventsGroupQueueShard } from '@openpanel/queue';
|
||||||
import { getRedisCache } from '@openpanel/redis';
|
import { getRedisCache } from '@openpanel/redis';
|
||||||
import type {
|
|
||||||
DecrementPayload,
|
import {
|
||||||
IdentifyPayload,
|
type IDecrementPayload,
|
||||||
IncrementPayload,
|
type IIdentifyPayload,
|
||||||
TrackHandlerPayload,
|
type IIncrementPayload,
|
||||||
TrackPayload,
|
type ITrackHandlerPayload,
|
||||||
} from '@openpanel/sdk';
|
type ITrackPayload,
|
||||||
|
zTrackHandlerPayload,
|
||||||
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
export function getStringHeaders(headers: FastifyRequest['headers']) {
|
export function getStringHeaders(headers: FastifyRequest['headers']) {
|
||||||
return Object.entries(
|
return Object.entries(
|
||||||
@@ -37,25 +39,28 @@ export function getStringHeaders(headers: FastifyRequest['headers']) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIdentity(body: TrackHandlerPayload): IdentifyPayload | undefined {
|
function getIdentity(body: ITrackHandlerPayload): IIdentifyPayload | undefined {
|
||||||
const identity =
|
if (body.type === 'track') {
|
||||||
'properties' in body.payload
|
const identity = body.payload.properties?.__identify as
|
||||||
? (body.payload?.properties?.__identify as IdentifyPayload | undefined)
|
| IIdentifyPayload
|
||||||
: undefined;
|
| undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
identity ||
|
identity ||
|
||||||
(body?.payload?.profileId
|
(body.payload.profileId
|
||||||
? {
|
? {
|
||||||
profileId: body.payload.profileId,
|
profileId: body.payload.profileId,
|
||||||
}
|
}
|
||||||
: undefined)
|
: undefined)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimestamp(
|
export function getTimestamp(
|
||||||
timestamp: FastifyRequest['timestamp'],
|
timestamp: FastifyRequest['timestamp'],
|
||||||
payload: TrackHandlerPayload['payload'],
|
payload: ITrackHandlerPayload['payload'],
|
||||||
) {
|
) {
|
||||||
const safeTimestamp = timestamp || Date.now();
|
const safeTimestamp = timestamp || Date.now();
|
||||||
const userDefinedTimestamp =
|
const userDefinedTimestamp =
|
||||||
@@ -82,7 +87,7 @@ export function getTimestamp(
|
|||||||
return { timestamp: safeTimestamp, isTimestampFromThePast: false };
|
return { timestamp: safeTimestamp, isTimestampFromThePast: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTimestampFromThePast is true only if timestamp is older than 1 hour
|
// isTimestampFromThePast is true only if timestamp is older than 15 minutes
|
||||||
const isTimestampFromThePast =
|
const isTimestampFromThePast =
|
||||||
clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS;
|
clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS;
|
||||||
|
|
||||||
@@ -92,170 +97,113 @@ export function getTimestamp(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handler(
|
interface TrackContext {
|
||||||
request: FastifyRequest<{
|
projectId: string;
|
||||||
Body: TrackHandlerPayload;
|
ip: string;
|
||||||
}>,
|
ua?: string;
|
||||||
reply: FastifyReply,
|
headers: Record<string, string | undefined>;
|
||||||
) {
|
timestamp: { value: number; isFromPast: boolean };
|
||||||
const timestamp = getTimestamp(request.timestamp, request.body.payload);
|
identity?: IIdentifyPayload;
|
||||||
const ip =
|
currentDeviceId?: string;
|
||||||
'properties' in request.body.payload &&
|
previousDeviceId?: string;
|
||||||
request.body.payload.properties?.__ip
|
geo: GeoLocation;
|
||||||
? (request.body.payload.properties.__ip as string)
|
}
|
||||||
: request.clientIp;
|
|
||||||
const ua = request.headers['user-agent'];
|
|
||||||
const projectId = request.client?.projectId;
|
|
||||||
|
|
||||||
|
async function buildContext(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Body: ITrackHandlerPayload;
|
||||||
|
}>,
|
||||||
|
validatedBody: ITrackHandlerPayload,
|
||||||
|
): Promise<TrackContext> {
|
||||||
|
const projectId = request.client?.projectId;
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
return reply.status(400).send({
|
throw new HttpError('Missing projectId', { status: 400 });
|
||||||
status: 400,
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Missing projectId',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const identity = getIdentity(request.body);
|
const timestamp = getTimestamp(request.timestamp, validatedBody.payload);
|
||||||
|
const ip =
|
||||||
|
validatedBody.type === 'track' && validatedBody.payload.properties?.__ip
|
||||||
|
? (validatedBody.payload.properties.__ip as string)
|
||||||
|
: request.clientIp;
|
||||||
|
const ua = request.headers['user-agent'];
|
||||||
|
const headers = getStringHeaders(request.headers);
|
||||||
|
|
||||||
|
const identity = getIdentity(validatedBody);
|
||||||
const profileId = identity?.profileId;
|
const profileId = identity?.profileId;
|
||||||
const overrideDeviceId = (() => {
|
|
||||||
const deviceId =
|
|
||||||
'properties' in request.body.payload
|
|
||||||
? request.body.payload.properties?.__deviceId
|
|
||||||
: undefined;
|
|
||||||
if (typeof deviceId === 'string') {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// We might get a profileId from the alias table
|
// We might get a profileId from the alias table
|
||||||
// If we do, we should use that instead of the one from the payload
|
// If we do, we should use that instead of the one from the payload
|
||||||
if (profileId) {
|
if (profileId && validatedBody.type === 'track') {
|
||||||
request.body.payload.profileId = profileId;
|
validatedBody.payload.profileId = profileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.body.type) {
|
// Get geo location (needed for track and identify)
|
||||||
case 'track': {
|
const geo = await getGeoLocation(ip);
|
||||||
const [salts, geo] = await Promise.all([getSalts(), getGeoLocation(ip)]);
|
|
||||||
const currentDeviceId =
|
// Generate device IDs if needed (for track)
|
||||||
overrideDeviceId ||
|
let currentDeviceId: string | undefined;
|
||||||
(ua
|
let previousDeviceId: string | undefined;
|
||||||
? generateDeviceId({
|
|
||||||
salt: salts.current,
|
if (validatedBody.type === 'track') {
|
||||||
origin: projectId,
|
const overrideDeviceId =
|
||||||
ip,
|
typeof validatedBody.payload.properties?.__deviceId === 'string'
|
||||||
ua,
|
? validatedBody.payload.properties.__deviceId
|
||||||
})
|
: undefined;
|
||||||
: '');
|
|
||||||
const previousDeviceId = ua
|
const [salts] = await Promise.all([getSalts()]);
|
||||||
|
currentDeviceId =
|
||||||
|
overrideDeviceId ||
|
||||||
|
(ua
|
||||||
? generateDeviceId({
|
? generateDeviceId({
|
||||||
salt: salts.previous,
|
salt: salts.current,
|
||||||
origin: projectId,
|
origin: projectId,
|
||||||
ip,
|
ip,
|
||||||
ua,
|
ua,
|
||||||
})
|
})
|
||||||
: '';
|
: '');
|
||||||
|
previousDeviceId = ua
|
||||||
const promises = [];
|
? generateDeviceId({
|
||||||
|
salt: salts.previous,
|
||||||
// If we have more than one property in the identity object, we should identify the user
|
origin: projectId,
|
||||||
// Otherwise its only a profileId and we should not identify the user
|
ip,
|
||||||
if (identity && Object.keys(identity).length > 1) {
|
ua,
|
||||||
promises.push(
|
})
|
||||||
identify({
|
: '';
|
||||||
payload: identity,
|
|
||||||
projectId,
|
|
||||||
geo,
|
|
||||||
ua,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(
|
|
||||||
track({
|
|
||||||
payload: request.body.payload,
|
|
||||||
currentDeviceId,
|
|
||||||
previousDeviceId,
|
|
||||||
projectId,
|
|
||||||
geo,
|
|
||||||
headers: getStringHeaders(request.headers),
|
|
||||||
timestamp: timestamp.timestamp,
|
|
||||||
isTimestampFromThePast: timestamp.isTimestampFromThePast,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'identify': {
|
|
||||||
const payload = request.body.payload;
|
|
||||||
const geo = await getGeoLocation(ip);
|
|
||||||
if (!payload.profileId) {
|
|
||||||
throw new HttpError('Missing profileId', {
|
|
||||||
status: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await identify({
|
|
||||||
payload,
|
|
||||||
projectId,
|
|
||||||
geo,
|
|
||||||
ua,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'alias': {
|
|
||||||
return reply.status(400).send({
|
|
||||||
status: 400,
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Alias is not supported',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case 'increment': {
|
|
||||||
await increment({
|
|
||||||
payload: request.body.payload,
|
|
||||||
projectId,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'decrement': {
|
|
||||||
await decrement({
|
|
||||||
payload: request.body.payload,
|
|
||||||
projectId,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return reply.status(400).send({
|
|
||||||
status: 400,
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Invalid type',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.status(200).send();
|
return {
|
||||||
|
projectId,
|
||||||
|
ip,
|
||||||
|
ua,
|
||||||
|
headers,
|
||||||
|
timestamp: {
|
||||||
|
value: timestamp.timestamp,
|
||||||
|
isFromPast: timestamp.isTimestampFromThePast,
|
||||||
|
},
|
||||||
|
identity,
|
||||||
|
currentDeviceId,
|
||||||
|
previousDeviceId,
|
||||||
|
geo,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function track({
|
async function handleTrack(
|
||||||
payload,
|
payload: ITrackPayload,
|
||||||
currentDeviceId,
|
context: TrackContext,
|
||||||
previousDeviceId,
|
): Promise<void> {
|
||||||
projectId,
|
const {
|
||||||
geo,
|
projectId,
|
||||||
headers,
|
currentDeviceId,
|
||||||
timestamp,
|
previousDeviceId,
|
||||||
isTimestampFromThePast,
|
geo,
|
||||||
}: {
|
headers,
|
||||||
payload: TrackPayload;
|
timestamp,
|
||||||
currentDeviceId: string;
|
} = context;
|
||||||
previousDeviceId: string;
|
|
||||||
projectId: string;
|
if (!currentDeviceId || !previousDeviceId) {
|
||||||
geo: GeoLocation;
|
throw new HttpError('Device ID generation failed', { status: 500 });
|
||||||
headers: Record<string, string | undefined>;
|
}
|
||||||
timestamp: number;
|
|
||||||
isTimestampFromThePast: boolean;
|
|
||||||
}) {
|
|
||||||
const uaInfo = parseUserAgent(headers['user-agent'], payload.properties);
|
const uaInfo = parseUserAgent(headers['user-agent'], payload.properties);
|
||||||
const groupId = uaInfo.isServer
|
const groupId = uaInfo.isServer
|
||||||
? payload.profileId
|
? payload.profileId
|
||||||
@@ -264,44 +212,51 @@ async function track({
|
|||||||
: currentDeviceId;
|
: currentDeviceId;
|
||||||
const jobId = [
|
const jobId = [
|
||||||
slug(payload.name),
|
slug(payload.name),
|
||||||
timestamp,
|
timestamp.value,
|
||||||
projectId,
|
projectId,
|
||||||
currentDeviceId,
|
currentDeviceId,
|
||||||
groupId,
|
groupId,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('-');
|
.join('-');
|
||||||
await getEventsGroupQueueShard(groupId).add({
|
|
||||||
orderMs: timestamp,
|
const promises = [];
|
||||||
data: {
|
|
||||||
projectId,
|
// If we have more than one property in the identity object, we should identify the user
|
||||||
headers,
|
// Otherwise its only a profileId and we should not identify the user
|
||||||
event: {
|
if (context.identity && Object.keys(context.identity).length > 1) {
|
||||||
...payload,
|
promises.push(handleIdentify(context.identity, context));
|
||||||
timestamp,
|
}
|
||||||
isTimestampFromThePast,
|
|
||||||
|
promises.push(
|
||||||
|
getEventsGroupQueueShard(groupId).add({
|
||||||
|
orderMs: timestamp.value,
|
||||||
|
data: {
|
||||||
|
projectId,
|
||||||
|
headers,
|
||||||
|
event: {
|
||||||
|
...payload,
|
||||||
|
timestamp: timestamp.value,
|
||||||
|
isTimestampFromThePast: timestamp.isFromPast,
|
||||||
|
},
|
||||||
|
uaInfo,
|
||||||
|
geo,
|
||||||
|
currentDeviceId,
|
||||||
|
previousDeviceId,
|
||||||
},
|
},
|
||||||
uaInfo,
|
groupId,
|
||||||
geo,
|
jobId,
|
||||||
currentDeviceId,
|
}),
|
||||||
previousDeviceId,
|
);
|
||||||
},
|
|
||||||
groupId,
|
await Promise.all(promises);
|
||||||
jobId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function identify({
|
async function handleIdentify(
|
||||||
payload,
|
payload: IIdentifyPayload,
|
||||||
projectId,
|
context: TrackContext,
|
||||||
geo,
|
): Promise<void> {
|
||||||
ua,
|
const { projectId, geo, ua } = context;
|
||||||
}: {
|
|
||||||
payload: IdentifyPayload;
|
|
||||||
projectId: string;
|
|
||||||
geo: GeoLocation;
|
|
||||||
ua?: string;
|
|
||||||
}) {
|
|
||||||
const uaInfo = parseUserAgent(ua, payload.properties);
|
const uaInfo = parseUserAgent(ua, payload.properties);
|
||||||
await upsertProfile({
|
await upsertProfile({
|
||||||
...payload,
|
...payload,
|
||||||
@@ -326,17 +281,15 @@ async function identify({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function increment({
|
async function adjustProfileProperty(
|
||||||
payload,
|
payload: IIncrementPayload | IDecrementPayload,
|
||||||
projectId,
|
projectId: string,
|
||||||
}: {
|
direction: 1 | -1,
|
||||||
payload: IncrementPayload;
|
): Promise<void> {
|
||||||
projectId: string;
|
|
||||||
}) {
|
|
||||||
const { profileId, property, value } = payload;
|
const { profileId, property, value } = payload;
|
||||||
const profile = await getProfileById(profileId, projectId);
|
const profile = await getProfileById(profileId, projectId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new Error('Not found');
|
throw new HttpError('Profile not found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = Number.parseInt(
|
const parsed = Number.parseInt(
|
||||||
@@ -345,12 +298,12 @@ async function increment({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (Number.isNaN(parsed)) {
|
if (Number.isNaN(parsed)) {
|
||||||
throw new Error('Not number');
|
throw new HttpError('Property value is not a number', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.properties = assocPath(
|
profile.properties = assocPath(
|
||||||
property.split('.'),
|
property.split('.'),
|
||||||
parsed + (value || 1),
|
parsed + direction * (value || 1),
|
||||||
profile.properties,
|
profile.properties,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -362,40 +315,74 @@ async function increment({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decrement({
|
async function handleIncrement(
|
||||||
payload,
|
payload: IIncrementPayload,
|
||||||
projectId,
|
context: TrackContext,
|
||||||
}: {
|
): Promise<void> {
|
||||||
payload: DecrementPayload;
|
await adjustProfileProperty(payload, context.projectId, 1);
|
||||||
projectId: string;
|
}
|
||||||
}) {
|
|
||||||
const { profileId, property, value } = payload;
|
async function handleDecrement(
|
||||||
const profile = await getProfileById(profileId, projectId);
|
payload: IDecrementPayload,
|
||||||
if (!profile) {
|
context: TrackContext,
|
||||||
throw new Error('Not found');
|
): Promise<void> {
|
||||||
|
await adjustProfileProperty(payload, context.projectId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handler(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Body: ITrackHandlerPayload;
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) {
|
||||||
|
// Validate request body with Zod
|
||||||
|
const validationResult = zTrackHandlerPayload.safeParse(request.body);
|
||||||
|
if (!validationResult.success) {
|
||||||
|
return reply.status(400).send({
|
||||||
|
status: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'Validation failed',
|
||||||
|
errors: validationResult.error.errors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = Number.parseInt(
|
const validatedBody = validationResult.data;
|
||||||
pathOr<string>('0', property.split('.'), profile.properties),
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Number.isNaN(parsed)) {
|
// Handle alias (not supported)
|
||||||
throw new Error('Not number');
|
if (validatedBody.type === 'alias') {
|
||||||
|
return reply.status(400).send({
|
||||||
|
status: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'Alias is not supported',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.properties = assocPath(
|
// Build request context
|
||||||
property.split('.'),
|
const context = await buildContext(request, validatedBody);
|
||||||
parsed - (value || 1),
|
|
||||||
profile.properties,
|
|
||||||
);
|
|
||||||
|
|
||||||
await upsertProfile({
|
// Dispatch to appropriate handler
|
||||||
id: profile.id,
|
switch (validatedBody.type) {
|
||||||
projectId,
|
case 'track':
|
||||||
properties: profile.properties,
|
await handleTrack(validatedBody.payload, context);
|
||||||
isExternal: true,
|
break;
|
||||||
});
|
case 'identify':
|
||||||
|
await handleIdentify(validatedBody.payload, context);
|
||||||
|
break;
|
||||||
|
case 'increment':
|
||||||
|
await handleIncrement(validatedBody.payload, context);
|
||||||
|
break;
|
||||||
|
case 'decrement':
|
||||||
|
await handleDecrement(validatedBody.payload, context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return reply.status(400).send({
|
||||||
|
status: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'Invalid type',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.status(200).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDeviceId(
|
export async function fetchDeviceId(
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
|
import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
|
||||||
import type { PostEventPayload, TrackHandlerPayload } from '@openpanel/sdk';
|
import type {
|
||||||
|
DeprecatedPostEventPayload,
|
||||||
|
ITrackHandlerPayload,
|
||||||
|
} from '@openpanel/validation';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export async function clientHook(
|
export async function clientHook(
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest<{
|
||||||
Body: PostEventPayload | TrackHandlerPayload;
|
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { isDuplicatedEvent } from '@/utils/deduplicate';
|
import { isDuplicatedEvent } from '@/utils/deduplicate';
|
||||||
import type { PostEventPayload, TrackHandlerPayload } from '@openpanel/sdk';
|
import type {
|
||||||
|
DeprecatedPostEventPayload,
|
||||||
|
ITrackHandlerPayload,
|
||||||
|
} from '@openpanel/validation';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export async function duplicateHook(
|
export async function duplicateHook(
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest<{
|
||||||
Body: PostEventPayload | TrackHandlerPayload;
|
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { isBot } from '@/bots';
|
import { isBot } from '@/bots';
|
||||||
import { createBotEvent } from '@openpanel/db';
|
import { createBotEvent } from '@openpanel/db';
|
||||||
import type { TrackHandlerPayload } from '@openpanel/sdk';
|
import type {
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
DeprecatedPostEventPayload,
|
||||||
|
ITrackHandlerPayload,
|
||||||
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
type DeprecatedEventPayload = {
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
name: string;
|
|
||||||
properties: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function isBotHook(
|
export async function isBotHook(
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest<{
|
||||||
Body: TrackHandlerPayload | DeprecatedEventPayload;
|
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -14,22 +14,6 @@ const trackRouter: FastifyPluginCallback = async (fastify) => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/',
|
url: '/',
|
||||||
handler: handler,
|
handler: handler,
|
||||||
schema: {
|
|
||||||
body: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['type', 'payload'],
|
|
||||||
properties: {
|
|
||||||
type: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['track', 'increment', 'decrement', 'alias', 'identify'],
|
|
||||||
},
|
|
||||||
payload: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { verifyPassword } from '@openpanel/common/server';
|
|||||||
import type { IServiceClientWithProject } from '@openpanel/db';
|
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||||
import { ClientType, getClientByIdCached } from '@openpanel/db';
|
import { ClientType, getClientByIdCached } from '@openpanel/db';
|
||||||
import { getCache } from '@openpanel/redis';
|
import { getCache } from '@openpanel/redis';
|
||||||
import type { PostEventPayload, TrackHandlerPayload } from '@openpanel/sdk';
|
|
||||||
import type {
|
import type {
|
||||||
|
DeprecatedPostEventPayload,
|
||||||
IProjectFilterIp,
|
IProjectFilterIp,
|
||||||
IProjectFilterProfileId,
|
IProjectFilterProfileId,
|
||||||
|
ITrackHandlerPayload,
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export class SdkAuthError extends Error {
|
|||||||
|
|
||||||
export async function validateSdkRequest(
|
export async function validateSdkRequest(
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest<{
|
||||||
Body: PostEventPayload | TrackHandlerPayload;
|
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
): Promise<IServiceClientWithProject> {
|
): Promise<IServiceClientWithProject> {
|
||||||
const { headers, clientIp } = req;
|
const { headers, clientIp } = req;
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ COPY packages/payments/package.json packages/payments/
|
|||||||
COPY packages/constants/package.json packages/constants/
|
COPY packages/constants/package.json packages/constants/
|
||||||
COPY packages/validation/package.json packages/validation/
|
COPY packages/validation/package.json packages/validation/
|
||||||
COPY packages/integrations/package.json packages/integrations/
|
COPY packages/integrations/package.json packages/integrations/
|
||||||
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
|
|
||||||
COPY packages/sdks/_info/package.json packages/sdks/_info/
|
COPY packages/sdks/_info/package.json packages/sdks/_info/
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
# Copy tracking script to self-hosting dashboard
|
# Copy tracking script to self-hosting dashboard
|
||||||
@@ -93,7 +92,6 @@ COPY --from=build /app/packages/payments/package.json ./packages/payments/
|
|||||||
COPY --from=build /app/packages/constants/package.json ./packages/constants/
|
COPY --from=build /app/packages/constants/package.json ./packages/constants/
|
||||||
COPY --from=build /app/packages/validation/package.json ./packages/validation/
|
COPY --from=build /app/packages/validation/package.json ./packages/validation/
|
||||||
COPY --from=build /app/packages/integrations/package.json ./packages/integrations/
|
COPY --from=build /app/packages/integrations/package.json ./packages/integrations/
|
||||||
COPY --from=build /app/packages/sdks/sdk/package.json ./packages/sdks/sdk/
|
|
||||||
COPY --from=build /app/packages/sdks/_info/package.json ./packages/sdks/_info/
|
COPY --from=build /app/packages/sdks/_info/package.json ./packages/sdks/_info/
|
||||||
COPY --from=build /app/patches ./patches
|
COPY --from=build /app/patches ./patches
|
||||||
|
|
||||||
@@ -132,7 +130,6 @@ COPY --from=build /app/packages/payments ./packages/payments
|
|||||||
COPY --from=build /app/packages/constants ./packages/constants
|
COPY --from=build /app/packages/constants ./packages/constants
|
||||||
COPY --from=build /app/packages/validation ./packages/validation
|
COPY --from=build /app/packages/validation ./packages/validation
|
||||||
COPY --from=build /app/packages/integrations ./packages/integrations
|
COPY --from=build /app/packages/integrations ./packages/integrations
|
||||||
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
|
|
||||||
COPY --from=build /app/packages/sdks/_info ./packages/sdks/_info
|
COPY --from=build /app/packages/sdks/_info ./packages/sdks/_info
|
||||||
COPY --from=build /app/tooling/typescript ./tooling/typescript
|
COPY --from=build /app/tooling/typescript ./tooling/typescript
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { differenceInDays, isSameDay, isSameMonth } from 'date-fns';
|
|||||||
export const DEFAULT_ASPECT_RATIO = 0.5625;
|
export const DEFAULT_ASPECT_RATIO = 0.5625;
|
||||||
export const NOT_SET_VALUE = '(not set)';
|
export const NOT_SET_VALUE = '(not set)';
|
||||||
|
|
||||||
|
export const RESERVED_EVENT_NAMES = [
|
||||||
|
'session_start',
|
||||||
|
'session_end',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const timeWindows = {
|
export const timeWindows = {
|
||||||
'30min': {
|
'30min': {
|
||||||
key: '30min',
|
key: '30min',
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ export type ILogger = winston.Logger;
|
|||||||
const logLevel = process.env.LOG_LEVEL ?? 'info';
|
const logLevel = process.env.LOG_LEVEL ?? 'info';
|
||||||
const silent = process.env.LOG_SILENT === 'true';
|
const silent = process.env.LOG_SILENT === 'true';
|
||||||
|
|
||||||
|
// Add colors for custom levels (fatal, warn, trace) that aren't in default color schemes
|
||||||
|
winston.addColors({
|
||||||
|
fatal: 'red',
|
||||||
|
warn: 'yellow',
|
||||||
|
trace: 'gray',
|
||||||
|
});
|
||||||
|
|
||||||
export function createLogger({ name }: { name: string }): ILogger {
|
export function createLogger({ name }: { name: string }): ILogger {
|
||||||
const service = [process.env.LOG_PREFIX, name, process.env.NODE_ENV ?? 'dev']
|
const service = [process.env.LOG_PREFIX, name, process.env.NODE_ENV ?? 'dev']
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"groupmq": "catalog:"
|
"groupmq": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openpanel/sdk": "workspace:*",
|
|
||||||
"@openpanel/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import type {
|
|||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import { createLogger } from '@openpanel/logger';
|
import { createLogger } from '@openpanel/logger';
|
||||||
import { getRedisGroupQueue, getRedisQueue } from '@openpanel/redis';
|
import { getRedisGroupQueue, getRedisQueue } from '@openpanel/redis';
|
||||||
import type { TrackPayload } from '@openpanel/sdk';
|
|
||||||
import { Queue as GroupQueue } from 'groupmq';
|
import { Queue as GroupQueue } from 'groupmq';
|
||||||
|
import type { ITrackPayload } from '../../validation';
|
||||||
|
|
||||||
export const EVENTS_GROUP_QUEUES_SHARDS = Number.parseInt(
|
export const EVENTS_GROUP_QUEUES_SHARDS = Number.parseInt(
|
||||||
process.env.EVENTS_GROUP_QUEUES_SHARDS || '1',
|
process.env.EVENTS_GROUP_QUEUES_SHARDS || '1',
|
||||||
@@ -32,7 +32,7 @@ export interface EventsQueuePayloadIncomingEvent {
|
|||||||
type: 'incomingEvent';
|
type: 'incomingEvent';
|
||||||
payload: {
|
payload: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
event: TrackPayload & {
|
event: ITrackPayload & {
|
||||||
timestamp: string | number;
|
timestamp: string | number;
|
||||||
isTimestampFromThePast: boolean;
|
isTimestampFromThePast: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +1 @@
|
|||||||
export * from './src/index';
|
export * from './src/index';
|
||||||
|
|
||||||
// Deprecated types for beta version of the SDKs
|
|
||||||
// Still used in api/event.controller.ts and api/profile.controller.ts
|
|
||||||
|
|
||||||
export interface OpenpanelEventOptions {
|
|
||||||
profileId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PostEventPayload {
|
|
||||||
name: string;
|
|
||||||
timestamp: string;
|
|
||||||
profileId?: string;
|
|
||||||
properties?: Record<string, unknown> & OpenpanelEventOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateProfilePayload {
|
|
||||||
profileId: string;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
email?: string;
|
|
||||||
avatar?: string;
|
|
||||||
properties?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IncrementProfilePayload {
|
|
||||||
profileId: string;
|
|
||||||
property: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DecrementProfilePayload {
|
|
||||||
profileId?: string;
|
|
||||||
property: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openpanel/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
|
"@openpanel/validation": "workspace:*",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"tsup": "^7.2.0",
|
"tsup": "^7.2.0",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,20 @@
|
|||||||
|
import type {
|
||||||
|
IAliasPayload as AliasPayload,
|
||||||
|
IDecrementPayload as DecrementPayload,
|
||||||
|
IIdentifyPayload as IdentifyPayload,
|
||||||
|
IIncrementPayload as IncrementPayload,
|
||||||
|
ITrackHandlerPayload as TrackHandlerPayload,
|
||||||
|
ITrackPayload as TrackPayload,
|
||||||
|
} from '@openpanel/validation';
|
||||||
import { Api } from './api';
|
import { Api } from './api';
|
||||||
|
|
||||||
export type TrackHandlerPayload =
|
export type {
|
||||||
| {
|
AliasPayload,
|
||||||
type: 'track';
|
DecrementPayload,
|
||||||
payload: TrackPayload;
|
IdentifyPayload,
|
||||||
}
|
IncrementPayload,
|
||||||
| {
|
TrackHandlerPayload,
|
||||||
type: 'increment';
|
TrackPayload,
|
||||||
payload: IncrementPayload;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'decrement';
|
|
||||||
payload: DecrementPayload;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'alias';
|
|
||||||
payload: AliasPayload;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'identify';
|
|
||||||
payload: IdentifyPayload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TrackPayload = {
|
|
||||||
name: string;
|
|
||||||
properties?: Record<string, unknown>;
|
|
||||||
profileId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackProperties = {
|
export type TrackProperties = {
|
||||||
@@ -33,32 +22,6 @@ export type TrackProperties = {
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IdentifyPayload = {
|
|
||||||
profileId: string;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
email?: string;
|
|
||||||
avatar?: string;
|
|
||||||
properties?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AliasPayload = {
|
|
||||||
profileId: string;
|
|
||||||
alias: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IncrementPayload = {
|
|
||||||
profileId: string;
|
|
||||||
property: string;
|
|
||||||
value?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DecrementPayload = {
|
|
||||||
profileId: string;
|
|
||||||
property: string;
|
|
||||||
value?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenPanelOptions = {
|
export type OpenPanelOptions = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
|
|||||||
@@ -555,3 +555,4 @@ export const zCreateImport = z.object({
|
|||||||
export type ICreateImport = z.infer<typeof zCreateImport>;
|
export type ICreateImport = z.infer<typeof zCreateImport>;
|
||||||
|
|
||||||
export * from './types.insights';
|
export * from './types.insights';
|
||||||
|
export * from './track.validation';
|
||||||
|
|||||||
104
packages/validation/src/track.validation.ts
Normal file
104
packages/validation/src/track.validation.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { RESERVED_EVENT_NAMES } from '@openpanel/constants';
|
||||||
|
|
||||||
|
export const zTrackPayload = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
properties: z.record(z.unknown()).optional(),
|
||||||
|
profileId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as any), {
|
||||||
|
message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(', ')}`,
|
||||||
|
path: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zIdentifyPayload = z.object({
|
||||||
|
profileId: z.string().min(1),
|
||||||
|
firstName: z.string().optional(),
|
||||||
|
lastName: z.string().optional(),
|
||||||
|
email: z.string().email().optional(),
|
||||||
|
avatar: z.string().url().optional(),
|
||||||
|
properties: z.record(z.unknown()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zIncrementPayload = z.object({
|
||||||
|
profileId: z.string().min(1),
|
||||||
|
property: z.string().min(1),
|
||||||
|
value: z.number().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zDecrementPayload = z.object({
|
||||||
|
profileId: z.string().min(1),
|
||||||
|
property: z.string().min(1),
|
||||||
|
value: z.number().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zAliasPayload = z.object({
|
||||||
|
profileId: z.string().min(1),
|
||||||
|
alias: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zTrackHandlerPayload = z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('track'),
|
||||||
|
payload: zTrackPayload,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('identify'),
|
||||||
|
payload: zIdentifyPayload,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('increment'),
|
||||||
|
payload: zIncrementPayload,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('decrement'),
|
||||||
|
payload: zDecrementPayload,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('alias'),
|
||||||
|
payload: zAliasPayload,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ITrackPayload = z.infer<typeof zTrackPayload>;
|
||||||
|
export type IIdentifyPayload = z.infer<typeof zIdentifyPayload>;
|
||||||
|
export type IIncrementPayload = z.infer<typeof zIncrementPayload>;
|
||||||
|
export type IDecrementPayload = z.infer<typeof zDecrementPayload>;
|
||||||
|
export type IAliasPayload = z.infer<typeof zAliasPayload>;
|
||||||
|
export type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;
|
||||||
|
|
||||||
|
// Deprecated types for beta version of the SDKs
|
||||||
|
|
||||||
|
export interface DeprecatedOpenpanelEventOptions {
|
||||||
|
profileId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeprecatedPostEventPayload {
|
||||||
|
name: string;
|
||||||
|
timestamp: string;
|
||||||
|
profileId?: string;
|
||||||
|
properties?: Record<string, unknown> & DeprecatedOpenpanelEventOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeprecatedUpdateProfilePayload {
|
||||||
|
profileId: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
avatar?: string;
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeprecatedIncrementProfilePayload {
|
||||||
|
profileId: string;
|
||||||
|
property: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeprecatedDecrementProfilePayload {
|
||||||
|
profileId?: string;
|
||||||
|
property: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -229,9 +229,6 @@ importers:
|
|||||||
'@faker-js/faker':
|
'@faker-js/faker':
|
||||||
specifier: ^9.0.1
|
specifier: ^9.0.1
|
||||||
version: 9.0.1
|
version: 9.0.1
|
||||||
'@openpanel/sdk':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/sdks/sdk
|
|
||||||
'@openpanel/tsconfig':
|
'@openpanel/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/typescript
|
version: link:../../tooling/typescript
|
||||||
@@ -1362,9 +1359,6 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.1.1-next.2(ioredis@5.8.2)
|
version: 1.1.1-next.2(ioredis@5.8.2)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@openpanel/sdk':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../sdks/sdk
|
|
||||||
'@openpanel/tsconfig':
|
'@openpanel/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/typescript
|
version: link:../../tooling/typescript
|
||||||
@@ -1558,6 +1552,9 @@ importers:
|
|||||||
'@openpanel/tsconfig':
|
'@openpanel/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../tooling/typescript
|
version: link:../../../tooling/typescript
|
||||||
|
'@openpanel/validation':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../validation
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 24.7.1
|
version: 24.7.1
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# nextjs-13-pages
|
|
||||||
rm -rf ../openpanel-examples/nextjs-13-pages/node_modules/@openpanel/nextjs && cp -R packages/sdks/nextjs ../openpanel-examples/nextjs-13-pages/node_modules/@openpanel/nextjs
|
|
||||||
rm -rf ../openpanel-examples/nextjs-13-pages/node_modules/@openpanel/web && cp -R packages/sdks/nextjs ../openpanel-examples/nextjs-13-pages/node_modules/@openpanel/web
|
|
||||||
# nextjs-app-dir
|
|
||||||
rm -rf ../openpanel-examples/nextjs-app-dir/node_modules/@openpanel/nextjs && cp -R packages/sdks/nextjs ../openpanel-examples/nextjs-app-dir/node_modules/@openpanel/nextjs
|
|
||||||
rm -rf ../openpanel-examples/nextjs-app-dir/node_modules/@openpanel/web && cp -R packages/sdks/nextjs ../openpanel-examples/nextjs-app-dir/node_modules/@openpanel/web
|
|
||||||
# expo-app
|
|
||||||
rm -rf ../openpanel-examples/expo-app/node_modules/@openpanel/react-native && cp -R packages/sdks/react-native ../openpanel-examples/expo-app/node_modules/@openpanel/react-native
|
|
||||||
rm -rf ../openpanel-examples/expo-app/node_modules/@openpanel/sdk && cp -R packages/sdks/sdk ../openpanel-examples/expo-app/node_modules/@openpanel/sdk
|
|
||||||
Reference in New Issue
Block a user