chore(root): migrate to biome
This commit is contained in:
@@ -7,8 +7,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -23,23 +21,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/db": "workspace:^",
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/sdk": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/progress": "^2.0.7",
|
||||
"@types/ramda": "^0.30.1",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function cli() {
|
||||
},
|
||||
{
|
||||
permissive: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const [command] = args._;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import zlib from 'zlib';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import zlib from 'node:zlib';
|
||||
import Progress from 'progress';
|
||||
import { assocPath, prop, uniqBy } from 'ramda';
|
||||
|
||||
@@ -37,17 +37,17 @@ function stripMixpanelProperties(obj: Record<string, unknown>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(
|
||||
([key]) =>
|
||||
!key.match(/^(\$|mp_)/) && !['time', 'distinct_id'].includes(key)
|
||||
)
|
||||
!key.match(/^(\$|mp_)/) && !['time', 'distinct_id'].includes(key),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function* parseJsonStream(
|
||||
fileStream: fs.ReadStream
|
||||
fileStream: fs.ReadStream,
|
||||
): AsyncGenerator<any, void, unknown> {
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
crlfDelay: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
|
||||
let buffer = '';
|
||||
@@ -100,7 +100,7 @@ function generateSessionEvents(events: IImportedEvent[]): Session[] {
|
||||
|
||||
events.sort(
|
||||
(a, b) =>
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
|
||||
);
|
||||
|
||||
for (const event of events) {
|
||||
@@ -172,7 +172,7 @@ function generateSessionEvents(events: IImportedEvent[]): Session[] {
|
||||
sessionList = sessionList.filter(
|
||||
(session) =>
|
||||
session.sessionId !== deviceSession?.sessionId &&
|
||||
session.sessionId !== profileSession?.sessionId
|
||||
session.sessionId !== profileSession?.sessionId,
|
||||
);
|
||||
sessionList.push(unifiedSession);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ function createEventObject(event: IMixpanelEvent): IImportedEvent {
|
||||
return {
|
||||
profile_id: event.properties.distinct_id
|
||||
? String(event.properties.distinct_id).replace(/^\$device:/, '')
|
||||
: event.properties.$device_id ?? '',
|
||||
: (event.properties.$device_id ?? ''),
|
||||
name: event.event,
|
||||
created_at: new Date(event.properties.time * 1000).toISOString(),
|
||||
properties: {
|
||||
@@ -284,7 +284,7 @@ function processEvents(events: IImportedEvent[]): IImportedEvent[] {
|
||||
...session.firstEvent,
|
||||
id: randomUUID(),
|
||||
created_at: new Date(
|
||||
new Date(session.firstEvent.created_at).getTime() - 1000
|
||||
new Date(session.firstEvent.created_at).getTime() - 1000,
|
||||
).toISOString(),
|
||||
session_id: session.sessionId,
|
||||
name: 'session_start',
|
||||
@@ -292,19 +292,19 @@ function processEvents(events: IImportedEvent[]): IImportedEvent[] {
|
||||
...uniqBy(
|
||||
prop('id'),
|
||||
session.events.map((event) =>
|
||||
assocPath(['session_id'], session.sessionId, event)
|
||||
)
|
||||
assocPath(['session_id'], session.sessionId, event),
|
||||
),
|
||||
),
|
||||
session.lastEvent && {
|
||||
...session.lastEvent,
|
||||
id: randomUUID(),
|
||||
created_at: new Date(
|
||||
new Date(session.lastEvent.created_at).getTime() + 1000
|
||||
new Date(session.lastEvent.created_at).getTime() + 1000,
|
||||
).toISOString(),
|
||||
session_id: session.sessionId,
|
||||
name: 'session_end',
|
||||
},
|
||||
].filter((item): item is IImportedEvent => !!item)
|
||||
].filter((item): item is IImportedEvent => !!item),
|
||||
);
|
||||
|
||||
return [
|
||||
@@ -325,7 +325,7 @@ async function sendBatchToAPI(
|
||||
apiUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
async function request() {
|
||||
const res = await fetch(`${apiUrl}/import/events`, {
|
||||
@@ -356,9 +356,9 @@ async function sendBatchToAPI(
|
||||
fs.writeFileSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
`openpanel/failed-import-batch-${batch[0]?.created_at ? new Date(batch[0]?.created_at).toISOString() : Date.now()}.json`
|
||||
`openpanel/failed-import-batch-${batch[0]?.created_at ? new Date(batch[0]?.created_at).toISOString() : Date.now()}.json`,
|
||||
),
|
||||
JSON.stringify(batch, null, 2)
|
||||
JSON.stringify(batch, null, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +380,7 @@ async function processFiles({
|
||||
{
|
||||
total: files.length,
|
||||
width: 20,
|
||||
}
|
||||
},
|
||||
);
|
||||
let savedEvents = 0;
|
||||
let currentBatch: IImportedEvent[] = [];
|
||||
@@ -415,8 +415,8 @@ async function processFiles({
|
||||
apiUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
apiBatching = [];
|
||||
}
|
||||
@@ -462,6 +462,6 @@ export async function importFiles({
|
||||
const endTime = Date.now();
|
||||
|
||||
console.log(
|
||||
`\nProcessing completed in ${(endTime - startTime) / 1000} seconds`
|
||||
`\nProcessing completed in ${(endTime - startTime) / 1000} seconds`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import arg from 'arg';
|
||||
import { glob } from 'glob';
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async function importer() {
|
||||
},
|
||||
{
|
||||
permissive: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!args['--glob']) {
|
||||
@@ -40,7 +40,7 @@ export default async function importer() {
|
||||
|
||||
const files = allFiles.slice(
|
||||
args['--from'] ?? 0,
|
||||
args['--to'] ?? Number.MAX_SAFE_INTEGER
|
||||
args['--to'] ?? Number.MAX_SAFE_INTEGER,
|
||||
);
|
||||
|
||||
if (args['--dry-run']) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from './src/crypto';
|
||||
export * from './src/profileId';
|
||||
export * from './src/date';
|
||||
export * from './src/object';
|
||||
export * from './src/names';
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -18,22 +16,11 @@
|
||||
"unique-names-generator": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@openpanel/validation": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
randomBytes,
|
||||
scrypt,
|
||||
timingSafeEqual,
|
||||
} from 'crypto';
|
||||
} from 'node:crypto';
|
||||
|
||||
export function generateSalt() {
|
||||
return randomBytes(16).toString('hex');
|
||||
@@ -16,7 +16,7 @@ export function generateSalt() {
|
||||
*/
|
||||
export async function hashPassword(
|
||||
password: string,
|
||||
keyLength = 32
|
||||
keyLength = 32,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// generate random 16 bytes long salt - recommended by NodeJS Docs
|
||||
@@ -38,7 +38,7 @@ export async function hashPassword(
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string,
|
||||
keyLength = 32
|
||||
keyLength = 32,
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const [salt, hashKey] = hash.split('.');
|
||||
2
packages/common/server/index.ts
Normal file
2
packages/common/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './crypto';
|
||||
export * from './profileId';
|
||||
@@ -84,7 +84,7 @@ export function completeSerie(
|
||||
data: ISerieDataItem[],
|
||||
_startDate: string,
|
||||
_endDate: string,
|
||||
interval: IInterval
|
||||
interval: IInterval,
|
||||
) {
|
||||
const startDate = parseISO(_startDate);
|
||||
const endDate = parseISO(_endDate);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { round } from './math';
|
||||
|
||||
export function getPreviousMetric(
|
||||
current: number,
|
||||
previous: number | null | undefined
|
||||
previous: number | null | undefined,
|
||||
): PreviousValue {
|
||||
if (isNil(previous)) {
|
||||
return undefined;
|
||||
@@ -20,7 +20,7 @@ export function getPreviousMetric(
|
||||
: 0) -
|
||||
1) *
|
||||
100,
|
||||
1
|
||||
1,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { isNumber } from 'mathjs';
|
||||
|
||||
export const round = (num: number, decimals = 2) => {
|
||||
const factor = Math.pow(10, decimals);
|
||||
const factor = 10 ** decimals;
|
||||
return Math.round((num + Number.EPSILON) * factor) / factor;
|
||||
};
|
||||
|
||||
export const average = (arr: (number | null)[]) => {
|
||||
const filtered = arr.filter(
|
||||
(n): n is number =>
|
||||
isNumber(n) && !Number.isNaN(n) && Number.isFinite(n) && n !== 0
|
||||
isNumber(n) && !Number.isNaN(n) && Number.isFinite(n) && n !== 0,
|
||||
);
|
||||
const avg = filtered.reduce((p, c) => p + c, 0) / filtered.length;
|
||||
return Number.isNaN(avg) ? 0 : avg;
|
||||
|
||||
@@ -3,7 +3,7 @@ import superjson from 'superjson';
|
||||
|
||||
export function toDots(
|
||||
obj: Record<string, unknown>,
|
||||
path = ''
|
||||
path = '',
|
||||
): Record<string, string> {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
@@ -26,7 +26,7 @@ export function toDots(
|
||||
}
|
||||
|
||||
export function toObject(
|
||||
obj: Record<string, string | undefined>
|
||||
obj: Record<string, string | undefined>,
|
||||
): Record<string, unknown> {
|
||||
let result: Record<string, unknown> = {};
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const slugify = (str: string) => {
|
||||
.replace('Å', 'A')
|
||||
.replace('Ä', 'A')
|
||||
.replace('Ö', 'O'),
|
||||
{ lower: true, strict: true, trim: true }
|
||||
{ lower: true, strict: true, trim: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function parseSearchParams(
|
||||
params: URLSearchParams
|
||||
params: URLSearchParams,
|
||||
): Record<string, string> | undefined {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of params.entries()) {
|
||||
@@ -39,7 +39,7 @@ export function parsePath(path?: string): {
|
||||
|
||||
export function isSameDomain(
|
||||
url1: string | undefined,
|
||||
url2: string | undefined
|
||||
url2: string | undefined,
|
||||
) {
|
||||
if (!url1 || !url2) {
|
||||
return false;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@/server/*": ["./src/server/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ export const metrics = {
|
||||
} as const;
|
||||
|
||||
export function isMinuteIntervalEnabledByRange(
|
||||
range: keyof typeof timeWindows
|
||||
range: keyof typeof timeWindows,
|
||||
) {
|
||||
return range === '30min' || range === 'lastHour';
|
||||
}
|
||||
@@ -165,13 +165,15 @@ export function isHourIntervalEnabledByRange(range: keyof typeof timeWindows) {
|
||||
}
|
||||
|
||||
export function getDefaultIntervalByRange(
|
||||
range: keyof typeof timeWindows
|
||||
range: keyof typeof timeWindows,
|
||||
): keyof typeof intervals {
|
||||
if (range === '30min' || range === 'lastHour') {
|
||||
return 'minute';
|
||||
} else if (range === 'today') {
|
||||
}
|
||||
if (range === 'today') {
|
||||
return 'hour';
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
range === '7d' ||
|
||||
range === '30d' ||
|
||||
range === 'lastMonth' ||
|
||||
@@ -184,12 +186,13 @@ export function getDefaultIntervalByRange(
|
||||
|
||||
export function getDefaultIntervalByDates(
|
||||
startDate: string | null,
|
||||
endDate: string | null
|
||||
endDate: string | null,
|
||||
): null | keyof typeof intervals {
|
||||
if (startDate && endDate) {
|
||||
if (isSameDay(startDate, endDate)) {
|
||||
return 'hour';
|
||||
} else if (isSameMonth(startDate, endDate)) {
|
||||
}
|
||||
if (isSameMonth(startDate, endDate)) {
|
||||
return 'day';
|
||||
}
|
||||
return 'month';
|
||||
|
||||
@@ -3,25 +3,12 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"date-fns": "^3.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"migrate:deploy:db": "pnpm with-env prisma migrate deploy",
|
||||
"migrate:deploy:ch": "pnpm goose up",
|
||||
"migrate:deploy": "pnpm migrate:deploy:db && pnpm migrate:deploy:ch",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
},
|
||||
@@ -29,23 +27,12 @@
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"@types/sqlstring": "^2.3.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ch, TABLE_NAMES } from '../clickhouse-client';
|
||||
import { TABLE_NAMES, ch } from '../clickhouse-client';
|
||||
import type { IClickhouseBotEvent } from '../services/event.service';
|
||||
import type {
|
||||
Find,
|
||||
|
||||
@@ -17,20 +17,20 @@ export type OnCompleted<T> =
|
||||
export type ProcessQueue<T> = (data: QueueItem<T>[]) => Promise<number[]>;
|
||||
|
||||
export type Find<T, R = unknown> = (
|
||||
callback: (item: QueueItem<T>) => boolean
|
||||
callback: (item: QueueItem<T>) => boolean,
|
||||
) => Promise<R | null>;
|
||||
|
||||
export type FindMany<T, R = unknown> = (
|
||||
callback: (item: QueueItem<T>) => boolean
|
||||
callback: (item: QueueItem<T>) => boolean,
|
||||
) => Promise<R[]>;
|
||||
|
||||
const getError = (e: unknown) => {
|
||||
if (e instanceof Error) {
|
||||
return [
|
||||
'Name: ' + e.name,
|
||||
'Message: ' + e.message,
|
||||
'Stack: ' + e.stack,
|
||||
'Cause: ' + (e.cause ? String(e.cause) : ''),
|
||||
`Name: ${e.name}`,
|
||||
`Message: ${e.message}`,
|
||||
`Stack: ${e.stack}`,
|
||||
`Cause: ${e.cause ? String(e.cause) : ''}`,
|
||||
].join('\n');
|
||||
}
|
||||
return 'Unknown error';
|
||||
@@ -59,13 +59,13 @@ export abstract class RedisBuffer<T> {
|
||||
this.table = options.table;
|
||||
this.batchSize = options.batchSize;
|
||||
this.disableAutoFlush = options.disableAutoFlush;
|
||||
this.logger = createLogger({ name: `buffer` }).child({
|
||||
this.logger = createLogger({ name: 'buffer' }).child({
|
||||
table: this.table,
|
||||
});
|
||||
}
|
||||
|
||||
public getKey(name?: string) {
|
||||
const key = this.prefix + ':' + this.table;
|
||||
const key = `${this.prefix}:${this.table}`;
|
||||
if (name) {
|
||||
return `${key}:${name}`;
|
||||
}
|
||||
@@ -78,12 +78,12 @@ export abstract class RedisBuffer<T> {
|
||||
|
||||
const length = await getRedisCache().llen(this.getKey());
|
||||
this.logger.debug(
|
||||
`Inserted item into buffer ${this.table}. Current length: ${length}`
|
||||
`Inserted item into buffer ${this.table}. Current length: ${length}`,
|
||||
);
|
||||
|
||||
if (!this.disableAutoFlush && this.batchSize && length >= this.batchSize) {
|
||||
this.logger.info(
|
||||
`Buffer ${this.table} reached batch size (${this.batchSize}). Flushing...`
|
||||
`Buffer ${this.table} reached batch size (${this.batchSize}). Flushing...`,
|
||||
);
|
||||
this.flush();
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export abstract class RedisBuffer<T> {
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Flushing ${queue.length} items from buffer ${this.table}`
|
||||
`Flushing ${queue.length} items from buffer ${this.table}`,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -112,19 +112,19 @@ export abstract class RedisBuffer<T> {
|
||||
if (this.onCompleted) {
|
||||
const res = await this.onCompleted(data);
|
||||
this.logger.info(
|
||||
`Completed processing ${res.length} items from buffer ${this.table}`
|
||||
`Completed processing ${res.length} items from buffer ${this.table}`,
|
||||
);
|
||||
return { count: res.length, data: res };
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Processed ${indexes.length} items from buffer ${this.table}`
|
||||
`Processed ${indexes.length} items from buffer ${this.table}`,
|
||||
);
|
||||
return { count: indexes.length, data: indexes };
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to process queue while flushing buffer ${this.table}:`,
|
||||
e
|
||||
e,
|
||||
);
|
||||
const timestamp = new Date().getTime();
|
||||
await getRedisCache().hset(this.getKey(`failed:${timestamp}`), {
|
||||
@@ -133,13 +133,13 @@ export abstract class RedisBuffer<T> {
|
||||
retries: 0,
|
||||
});
|
||||
this.logger.warn(
|
||||
`Stored ${queue.length} failed items in ${this.getKey(`failed:${timestamp}`)}`
|
||||
`Stored ${queue.length} failed items in ${this.getKey(`failed:${timestamp}`)}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to get queue while flushing buffer ${this.table}:`,
|
||||
e
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export abstract class RedisBuffer<T> {
|
||||
multi.lrem(this.getKey(), 0, DELETE);
|
||||
await multi.exec();
|
||||
this.logger.debug(
|
||||
`Deleted ${indexes.length} items from buffer ${this.table}`
|
||||
`Deleted ${indexes.length} items from buffer ${this.table}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export abstract class RedisBuffer<T> {
|
||||
}))
|
||||
.filter((item): item is QueueItem<T> => item.event !== null);
|
||||
this.logger.debug(
|
||||
`Retrieved ${result.length} items from buffer ${this.table}`
|
||||
`Retrieved ${result.length} items from buffer ${this.table}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import SuperJSON from 'superjson';
|
||||
import { deepMergeObjects } from '@openpanel/common';
|
||||
import { getRedisCache, getRedisPub } from '@openpanel/redis';
|
||||
|
||||
import { ch, TABLE_NAMES } from '../clickhouse-client';
|
||||
import { TABLE_NAMES, ch } from '../clickhouse-client';
|
||||
import { transformEvent } from '../services/event.service';
|
||||
import type {
|
||||
IClickhouseEvent,
|
||||
@@ -22,7 +22,7 @@ import { RedisBuffer } from './buffer';
|
||||
|
||||
const sortOldestFirst = (
|
||||
a: QueueItem<IClickhouseEvent>,
|
||||
b: QueueItem<IClickhouseEvent>
|
||||
b: QueueItem<IClickhouseEvent>,
|
||||
) =>
|
||||
new Date(a.event.created_at).getTime() -
|
||||
new Date(b.event.created_at).getTime();
|
||||
@@ -37,25 +37,25 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
public onInsert?: OnInsert<IClickhouseEvent> | undefined = (event) => {
|
||||
getRedisPub().publish(
|
||||
'event:received',
|
||||
SuperJSON.stringify(transformEvent(event))
|
||||
SuperJSON.stringify(transformEvent(event)),
|
||||
);
|
||||
if (event.profile_id) {
|
||||
getRedisCache().set(
|
||||
`live:event:${event.project_id}:${event.profile_id}`,
|
||||
'',
|
||||
'EX',
|
||||
60 * 5
|
||||
60 * 5,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public onCompleted?: OnCompleted<IClickhouseEvent> | undefined = (
|
||||
savedEvents
|
||||
savedEvents,
|
||||
) => {
|
||||
for (const event of savedEvents) {
|
||||
getRedisPub().publish(
|
||||
'event:saved',
|
||||
SuperJSON.stringify(transformEvent(event))
|
||||
SuperJSON.stringify(transformEvent(event)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
queue
|
||||
.filter(
|
||||
(item) =>
|
||||
item.event.name !== 'screen_view' || item.event.device === 'server'
|
||||
item.event.name !== 'screen_view' || item.event.device === 'server',
|
||||
)
|
||||
.forEach((item) => {
|
||||
// Find the last event with data and merge it with the current event
|
||||
@@ -93,7 +93,7 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
|
||||
const event = deepMergeObjects<IClickhouseEvent>(
|
||||
omit(['properties', 'duration'], lastEventWithData?.event || {}),
|
||||
item.event
|
||||
item.event,
|
||||
);
|
||||
|
||||
if (lastEventWithData) {
|
||||
@@ -111,8 +111,8 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
(item) => item.event.session_id,
|
||||
queue.filter(
|
||||
(item) =>
|
||||
item.event.name === 'screen_view' && item.event.device !== 'server'
|
||||
)
|
||||
item.event.name === 'screen_view' && item.event.device !== 'server',
|
||||
),
|
||||
);
|
||||
|
||||
// Iterate over each group
|
||||
@@ -125,7 +125,7 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
const hasSessionEnd = queue.find(
|
||||
(item) =>
|
||||
item.event.name === 'session_end' &&
|
||||
item.event.session_id === sessionId
|
||||
item.event.session_id === sessionId,
|
||||
);
|
||||
|
||||
screenViews
|
||||
@@ -187,7 +187,7 @@ export class EventBuffer extends RedisBuffer<IClickhouseEvent> {
|
||||
};
|
||||
|
||||
public findMany: FindMany<IClickhouseEvent, IServiceEvent> = async (
|
||||
callback
|
||||
callback,
|
||||
) => {
|
||||
return this.getQueue(-1)
|
||||
.then((queue) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mergeDeepRight } from 'ramda';
|
||||
import { toDots } from '@openpanel/common';
|
||||
import { getRedisCache } from '@openpanel/redis';
|
||||
|
||||
import { ch, chQuery, TABLE_NAMES } from '../clickhouse-client';
|
||||
import { TABLE_NAMES, ch, chQuery } from '../clickhouse-client';
|
||||
import type {
|
||||
IClickhouseProfile,
|
||||
IServiceProfile,
|
||||
@@ -35,13 +35,13 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
const cleanedQueue = this.combineQueueItems(queue);
|
||||
const redisProfiles = await this.getCachedProfiles(cleanedQueue);
|
||||
const dbProfiles = await this.fetchDbProfiles(
|
||||
cleanedQueue.filter((_, index) => !redisProfiles[index])
|
||||
cleanedQueue.filter((_, index) => !redisProfiles[index]),
|
||||
);
|
||||
|
||||
const values = this.createProfileValues(
|
||||
cleanedQueue,
|
||||
redisProfiles,
|
||||
dbProfiles
|
||||
dbProfiles,
|
||||
);
|
||||
|
||||
if (values.length > 0) {
|
||||
@@ -55,7 +55,7 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
private matchPartialObject(
|
||||
full: any,
|
||||
partial: any,
|
||||
options: { ignore: string[] }
|
||||
options: { ignore: string[] },
|
||||
): boolean {
|
||||
if (typeof partial !== 'object' || partial === null) {
|
||||
return partial === full;
|
||||
@@ -78,7 +78,7 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
}
|
||||
|
||||
private combineQueueItems(
|
||||
queue: QueueItem<IClickhouseProfile>[]
|
||||
queue: QueueItem<IClickhouseProfile>[],
|
||||
): QueueItem<IClickhouseProfile>[] {
|
||||
const itemsToClickhouse = new Map<string, QueueItem<IClickhouseProfile>>();
|
||||
|
||||
@@ -92,11 +92,11 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
}
|
||||
|
||||
private async getCachedProfiles(
|
||||
cleanedQueue: QueueItem<IClickhouseProfile>[]
|
||||
cleanedQueue: QueueItem<IClickhouseProfile>[],
|
||||
): Promise<(IClickhouseProfile | null)[]> {
|
||||
const redisCache = getRedisCache();
|
||||
const keys = cleanedQueue.map(
|
||||
(item) => `profile:${item.event.project_id}:${item.event.id}`
|
||||
(item) => `profile:${item.event.project_id}:${item.event.id}`,
|
||||
);
|
||||
const cachedProfiles = await redisCache.mget(...keys);
|
||||
return cachedProfiles.map((profile) => {
|
||||
@@ -109,7 +109,7 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
}
|
||||
|
||||
private async fetchDbProfiles(
|
||||
cleanedQueue: QueueItem<IClickhouseProfile>[]
|
||||
cleanedQueue: QueueItem<IClickhouseProfile>[],
|
||||
): Promise<IClickhouseProfile[]> {
|
||||
if (cleanedQueue.length === 0) {
|
||||
return [];
|
||||
@@ -122,21 +122,21 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
WHERE
|
||||
(id, project_id) IN (${cleanedQueue.map((item) => `('${item.event.id}', '${item.event.project_id}')`).join(',')})
|
||||
ORDER BY
|
||||
created_at DESC`
|
||||
created_at DESC`,
|
||||
);
|
||||
}
|
||||
|
||||
private createProfileValues(
|
||||
cleanedQueue: QueueItem<IClickhouseProfile>[],
|
||||
redisProfiles: (IClickhouseProfile | null)[],
|
||||
dbProfiles: IClickhouseProfile[]
|
||||
dbProfiles: IClickhouseProfile[],
|
||||
): IClickhouseProfile[] {
|
||||
return cleanedQueue
|
||||
.map((item, index) => {
|
||||
const cachedProfile = redisProfiles[index];
|
||||
const dbProfile = dbProfiles.find(
|
||||
(p) =>
|
||||
p.id === item.event.id && p.project_id === item.event.project_id
|
||||
p.id === item.event.id && p.project_id === item.event.project_id,
|
||||
);
|
||||
const profile = cachedProfile || dbProfile;
|
||||
|
||||
@@ -150,7 +150,7 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
},
|
||||
{
|
||||
ignore: ['created_at'],
|
||||
}
|
||||
},
|
||||
)
|
||||
) {
|
||||
console.log('Ignoring profile', item.event.id);
|
||||
@@ -182,14 +182,14 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
multi.setex(
|
||||
`profile:${value.project_id}:${value.id}`,
|
||||
60 * 30, // 30 minutes
|
||||
JSON.stringify(value)
|
||||
JSON.stringify(value),
|
||||
);
|
||||
});
|
||||
await multi.exec();
|
||||
}
|
||||
|
||||
private async insertIntoClickhouse(
|
||||
values: IClickhouseProfile[]
|
||||
values: IClickhouseProfile[],
|
||||
): Promise<void> {
|
||||
await ch.insert({
|
||||
table: TABLE_NAMES.profiles,
|
||||
@@ -199,7 +199,7 @@ export class ProfileBuffer extends RedisBuffer<IClickhouseProfile> {
|
||||
}
|
||||
|
||||
public findMany: FindMany<IClickhouseProfile, IServiceProfile> = async (
|
||||
callback
|
||||
callback,
|
||||
) => {
|
||||
return this.getQueue(-1)
|
||||
.then((queue) => {
|
||||
|
||||
@@ -53,19 +53,19 @@ export const ch = new Proxy(originalCh, {
|
||||
error.message.includes('socket hang up') ||
|
||||
error.message.includes('Timeout error'))
|
||||
) {
|
||||
childLogger.error(`Captured error`, {
|
||||
childLogger.error('Captured error', {
|
||||
error,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
try {
|
||||
// Retry once
|
||||
childLogger.info(`Retrying query`);
|
||||
childLogger.info('Retrying query');
|
||||
if (property in target) {
|
||||
// @ts-expect-error
|
||||
return await target[property](...args);
|
||||
}
|
||||
} catch (retryError) {
|
||||
logger.error(`Retry failed`, retryError);
|
||||
logger.error('Retry failed', retryError);
|
||||
throw retryError; // Rethrow or handle as needed
|
||||
}
|
||||
} else {
|
||||
@@ -85,7 +85,7 @@ export const ch = new Proxy(originalCh, {
|
||||
});
|
||||
|
||||
export async function chQueryWithMeta<T extends Record<string, any>>(
|
||||
query: string
|
||||
query: string,
|
||||
): Promise<ResponseJSON<T>> {
|
||||
const start = Date.now();
|
||||
const res = await ch.query({
|
||||
@@ -102,7 +102,7 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
|
||||
...acc,
|
||||
[key]:
|
||||
item[key] && meta?.type.includes('Int')
|
||||
? parseFloat(item[key] as string)
|
||||
? Number.parseFloat(item[key] as string)
|
||||
: item[key],
|
||||
};
|
||||
}, {} as T);
|
||||
@@ -120,14 +120,14 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
|
||||
}
|
||||
|
||||
export async function chQuery<T extends Record<string, any>>(
|
||||
query: string
|
||||
query: string,
|
||||
): Promise<T[]> {
|
||||
return (await chQueryWithMeta<T>(query)).data;
|
||||
}
|
||||
|
||||
export function formatClickhouseDate(
|
||||
_date: Date | string,
|
||||
skipTime = false
|
||||
skipTime = false,
|
||||
): string {
|
||||
const date = typeof _date === 'string' ? new Date(_date) : _date;
|
||||
if (skipTime) {
|
||||
@@ -153,5 +153,5 @@ export function toDate(str: string, interval?: IInterval) {
|
||||
}
|
||||
|
||||
export function convertClickhouseDateToJs(date: string) {
|
||||
return new Date(date.replace(' ', 'T') + 'Z');
|
||||
return new Date(`${date.replace(' ', 'T')}Z`);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
} from '@openpanel/validation';
|
||||
|
||||
import {
|
||||
formatClickhouseDate,
|
||||
TABLE_NAMES,
|
||||
formatClickhouseDate,
|
||||
toDate,
|
||||
} from '../clickhouse-client';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
@@ -32,7 +32,7 @@ export function getSelectPropertyKey(property: string) {
|
||||
if (property.startsWith('properties.')) {
|
||||
if (property.includes('*')) {
|
||||
return `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
transformPropertyKey(property)
|
||||
transformPropertyKey(property),
|
||||
)})))`;
|
||||
}
|
||||
return `properties['${property.replace(/^properties\./, '')}']`;
|
||||
@@ -64,7 +64,7 @@ export function getChartSql({
|
||||
sb.select.label_0 = `'*' as label_0`;
|
||||
}
|
||||
|
||||
sb.select.count = `count(*) as count`;
|
||||
sb.select.count = 'count(*) as count';
|
||||
switch (interval) {
|
||||
case 'minute': {
|
||||
sb.select.date = `toStartOfMinute(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
|
||||
@@ -111,15 +111,16 @@ export function getChartSql({
|
||||
});
|
||||
|
||||
if (event.segment === 'user') {
|
||||
sb.select.count = `countDistinct(profile_id) as count`;
|
||||
sb.select.count = 'countDistinct(profile_id) as count';
|
||||
}
|
||||
|
||||
if (event.segment === 'session') {
|
||||
sb.select.count = `countDistinct(session_id) as count`;
|
||||
sb.select.count = 'countDistinct(session_id) as count';
|
||||
}
|
||||
|
||||
if (event.segment === 'user_average') {
|
||||
sb.select.count = `COUNT(*)::float / COUNT(DISTINCT profile_id)::float as count`;
|
||||
sb.select.count =
|
||||
'COUNT(*)::float / COUNT(DISTINCT profile_id)::float as count';
|
||||
}
|
||||
|
||||
if (event.segment === 'property_sum' && event.property) {
|
||||
@@ -136,7 +137,7 @@ export function getChartSql({
|
||||
sb.from = `(
|
||||
SELECT DISTINCT ON (profile_id) * from ${TABLE_NAMES.events} WHERE ${join(
|
||||
sb.where,
|
||||
' AND '
|
||||
' AND ',
|
||||
)}
|
||||
ORDER BY profile_id, created_at DESC
|
||||
) as subQuery`;
|
||||
@@ -157,9 +158,9 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
|
||||
if (name === 'has_profile') {
|
||||
if (value.includes('true')) {
|
||||
where[id] = `profile_id != device_id`;
|
||||
where[id] = 'profile_id != device_id';
|
||||
} else {
|
||||
where[id] = `profile_id = device_id`;
|
||||
where[id] = 'profile_id = device_id';
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +212,7 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) =>
|
||||
`${whereFrom} LIKE ${escape(`%${String(val).trim()}%`)}`
|
||||
`${whereFrom} LIKE ${escape(`%${String(val).trim()}%`)}`,
|
||||
)
|
||||
.join(' OR ');
|
||||
}
|
||||
@@ -226,7 +227,7 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) =>
|
||||
`${whereFrom} NOT LIKE ${escape(`%${String(val).trim()}%`)}`
|
||||
`${whereFrom} NOT LIKE ${escape(`%${String(val).trim()}%`)}`,
|
||||
)
|
||||
.join(' OR ');
|
||||
}
|
||||
@@ -240,7 +241,8 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
} else {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) => `${whereFrom} LIKE ${escape(`${String(val).trim()}%`)}`
|
||||
(val) =>
|
||||
`${whereFrom} LIKE ${escape(`${String(val).trim()}%`)}`,
|
||||
)
|
||||
.join(' OR ');
|
||||
}
|
||||
@@ -254,7 +256,8 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
} else {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) => `${whereFrom} LIKE ${escape(`%${String(val).trim()}`)}`
|
||||
(val) =>
|
||||
`${whereFrom} LIKE ${escape(`%${String(val).trim()}`)}`,
|
||||
)
|
||||
.join(' OR ');
|
||||
}
|
||||
@@ -268,7 +271,7 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
} else {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) => `match(${whereFrom}, ${escape(String(val).trim())})`
|
||||
(val) => `match(${whereFrom}, ${escape(String(val).trim())})`,
|
||||
)
|
||||
.join(' OR ');
|
||||
}
|
||||
@@ -306,7 +309,7 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
case 'doesNotContain': {
|
||||
where[id] = value
|
||||
.map(
|
||||
(val) => `${name} NOT LIKE ${escape(`%${String(val).trim()}%`)}`
|
||||
(val) => `${name} NOT LIKE ${escape(`%${String(val).trim()}%`)}`,
|
||||
)
|
||||
.join(' OR ');
|
||||
break;
|
||||
|
||||
@@ -8,11 +8,11 @@ import type { IChartEventFilter } from '@openpanel/validation';
|
||||
|
||||
import { botBuffer, eventBuffer } from '../buffers';
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
ch,
|
||||
chQuery,
|
||||
convertClickhouseDateToJs,
|
||||
formatClickhouseDate,
|
||||
TABLE_NAMES,
|
||||
} from '../clickhouse-client';
|
||||
import type { EventMeta, Prisma } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
@@ -211,7 +211,7 @@ function maskString(str: string, mask = '*') {
|
||||
}
|
||||
|
||||
export function transformMinimalEvent(
|
||||
event: IServiceEvent
|
||||
event: IServiceEvent,
|
||||
): IServiceEventMinimal {
|
||||
return {
|
||||
id: event.id,
|
||||
@@ -242,7 +242,7 @@ export async function getLiveVisitors(projectId: string) {
|
||||
|
||||
export async function getEvents(
|
||||
sql: string,
|
||||
options: GetEventsOptions = {}
|
||||
options: GetEventsOptions = {},
|
||||
): Promise<IServiceEvent[]> {
|
||||
const events = await chQuery<IClickhouseEvent>(sql);
|
||||
const projectId = events[0]?.project_id;
|
||||
@@ -278,7 +278,7 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
|
||||
payload.profileId = payload.deviceId;
|
||||
}
|
||||
console.log(
|
||||
`create event ${payload.name} for [deviceId]: ${payload.deviceId} [profileId]: ${payload.profileId} [projectId]: ${payload.projectId} [path]: ${payload.path}`
|
||||
`create event ${payload.name} for [deviceId]: ${payload.deviceId} [profileId]: ${payload.profileId} [projectId]: ${payload.projectId} [path]: ${payload.path}`,
|
||||
);
|
||||
|
||||
if (payload.profileId !== '') {
|
||||
@@ -389,7 +389,7 @@ export async function getEventList({
|
||||
os: true,
|
||||
browser: true,
|
||||
},
|
||||
incomingSelect ?? {}
|
||||
incomingSelect ?? {},
|
||||
);
|
||||
|
||||
if (select.id) {
|
||||
@@ -491,7 +491,7 @@ export async function getEventList({
|
||||
if (events && events.length > 0) {
|
||||
sb.where.events = `name IN (${join(
|
||||
events.map((event) => escape(event)),
|
||||
','
|
||||
',',
|
||||
)})`;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ export async function getEventsCount({
|
||||
if (events && events.length > 0) {
|
||||
sb.where.events = `name IN (${join(
|
||||
events.map((event) => escape(event)),
|
||||
','
|
||||
',',
|
||||
)})`;
|
||||
}
|
||||
|
||||
@@ -549,7 +549,7 @@ export async function getEventsCount({
|
||||
}
|
||||
|
||||
const res = await chQuery<{ count: number }>(
|
||||
getSql().replace('*', 'count(*) as count')
|
||||
getSql().replace('*', 'count(*) as count'),
|
||||
);
|
||||
|
||||
return res[0]?.count ?? 0;
|
||||
@@ -593,7 +593,7 @@ export async function getLastScreenViewFromProfileId({
|
||||
}
|
||||
|
||||
const eventInBuffer = await eventBuffer.find(
|
||||
(item) => item.event.profile_id === profileId
|
||||
(item) => item.event.profile_id === profileId,
|
||||
);
|
||||
|
||||
if (eventInBuffer) {
|
||||
@@ -602,7 +602,7 @@ export async function getLastScreenViewFromProfileId({
|
||||
|
||||
const [eventInDb] = profileId
|
||||
? await getEvents(
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND profile_id = ${escape(profileId)} AND project_id = ${escape(projectId)} AND created_at >= now() - INTERVAL 30 MINUTE ORDER BY created_at DESC LIMIT 1`
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND profile_id = ${escape(profileId)} AND project_id = ${escape(projectId)} AND created_at >= now() - INTERVAL 30 MINUTE ORDER BY created_at DESC LIMIT 1`,
|
||||
)
|
||||
: [];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { db } from '../prisma-client';
|
||||
|
||||
export async function getId(
|
||||
tableName: 'project' | 'dashboard' | 'organization',
|
||||
name: string
|
||||
name: string,
|
||||
) {
|
||||
const newId = slug(name);
|
||||
if (!db[tableName]) {
|
||||
|
||||
@@ -7,10 +7,10 @@ import type { IChartEventFilter } from '@openpanel/validation';
|
||||
|
||||
import { profileBuffer } from '../buffers';
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
ch,
|
||||
chQuery,
|
||||
formatClickhouseDate,
|
||||
TABLE_NAMES,
|
||||
} from '../clickhouse-client';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function getProfileById(id: string, projectId: string) {
|
||||
}
|
||||
|
||||
const [profile] = await chQuery<IClickhouseProfile>(
|
||||
`SELECT * FROM ${TABLE_NAMES.profiles} WHERE id = ${escape(String(id))} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
|
||||
`SELECT * FROM ${TABLE_NAMES.profiles} WHERE id = ${escape(String(id))} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`,
|
||||
);
|
||||
|
||||
if (!profile) {
|
||||
@@ -82,7 +82,7 @@ export async function getProfiles(ids: string[], projectId: string) {
|
||||
WHERE
|
||||
project_id = ${escape(projectId)} AND
|
||||
id IN (${filteredIds.map((id) => escape(id)).join(',')})
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
return data.map(transformProfile);
|
||||
@@ -251,7 +251,7 @@ export async function getProfileId({
|
||||
profile_id: string;
|
||||
project_id: string;
|
||||
}>(
|
||||
`SELECT * FROM ${TABLE_NAMES.alias} WHERE project_id = '${projectId}' AND (alias = '${profileId}' OR profile_id = '${profileId}')`
|
||||
`SELECT * FROM ${TABLE_NAMES.alias} WHERE project_id = '${projectId}' AND (alias = '${profileId}' OR profile_id = '${profileId}')`,
|
||||
);
|
||||
|
||||
if (res[0]) {
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function getCurrentProjects(organizationSlug: string) {
|
||||
|
||||
if (access.length > 0) {
|
||||
return projects.filter((project) =>
|
||||
access.some((a) => a.projectId === project.id)
|
||||
access.some((a) => a.projectId === project.id),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,20 +19,20 @@ export type IServiceReport = Awaited<ReturnType<typeof getReportById>>;
|
||||
|
||||
export function transformFilter(
|
||||
filter: Partial<IChartEventFilter>,
|
||||
index: number
|
||||
index: number,
|
||||
): IChartEventFilter {
|
||||
return {
|
||||
id: filter.id ?? alphabetIds[index] ?? 'A',
|
||||
name: filter.name ?? 'Unknown Filter',
|
||||
operator: filter.operator ?? 'is',
|
||||
value:
|
||||
typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
|
||||
typeof filter.value === 'string' ? [filter.value] : (filter.value ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
export function transformReportEvent(
|
||||
event: Partial<IChartEvent>,
|
||||
index: number
|
||||
index: number,
|
||||
): IChartEvent {
|
||||
return {
|
||||
segment: event.segment ?? 'event',
|
||||
@@ -45,7 +45,7 @@ export function transformReportEvent(
|
||||
}
|
||||
|
||||
export function transformReport(
|
||||
report: DbReport
|
||||
report: DbReport,
|
||||
): IChartProps & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, TABLE_NAMES } from '../clickhouse-client';
|
||||
import { TABLE_NAMES, chQuery } from '../clickhouse-client';
|
||||
|
||||
type IGetWeekRetentionInput = {
|
||||
projectId: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { generateSalt } from '@openpanel/common';
|
||||
import { generateSalt } from '@openpanel/common/server';
|
||||
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
@@ -61,17 +61,14 @@ export async function createInitialSalts() {
|
||||
} else {
|
||||
console.log('Error getting salts', error);
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
const delay = BASE_DELAY * Math.pow(2, retryCount);
|
||||
const delay = BASE_DELAY * 2 ** retryCount;
|
||||
console.log(
|
||||
`Retrying in ${delay}ms... (Attempt ${retryCount + 1}/${MAX_RETRIES})`
|
||||
`Retrying in ${delay}ms... (Attempt ${retryCount + 1}/${MAX_RETRIES})`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return createSaltsWithRetry(retryCount + 1);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to create salts after ${MAX_RETRIES} attempts`
|
||||
);
|
||||
}
|
||||
throw new Error(`Failed to create salts after ${MAX_RETRIES} attempts`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,16 +27,16 @@ export function createSqlBuilder() {
|
||||
};
|
||||
|
||||
const getWhere = () =>
|
||||
Object.keys(sb.where).length ? 'WHERE ' + join(sb.where, ' AND ') : '';
|
||||
Object.keys(sb.where).length ? `WHERE ${join(sb.where, ' AND ')}` : '';
|
||||
const getHaving = () =>
|
||||
Object.keys(sb.having).length ? 'HAVING ' + join(sb.having, ' AND ') : '';
|
||||
Object.keys(sb.having).length ? `HAVING ${join(sb.having, ' AND ')}` : '';
|
||||
const getFrom = () => `FROM ${sb.from}`;
|
||||
const getSelect = () =>
|
||||
'SELECT ' + (Object.keys(sb.select).length ? join(sb.select, ', ') : '*');
|
||||
`SELECT ${Object.keys(sb.select).length ? join(sb.select, ', ') : '*'}`;
|
||||
const getGroupBy = () =>
|
||||
Object.keys(sb.groupBy).length ? 'GROUP BY ' + join(sb.groupBy, ', ') : '';
|
||||
Object.keys(sb.groupBy).length ? `GROUP BY ${join(sb.groupBy, ', ')}` : '';
|
||||
const getOrderBy = () =>
|
||||
Object.keys(sb.orderBy).length ? 'ORDER BY ' + join(sb.orderBy, ', ') : '';
|
||||
Object.keys(sb.orderBy).length ? `ORDER BY ${join(sb.orderBy, ', ')}` : '';
|
||||
const getLimit = () => (sb.limit ? `LIMIT ${sb.limit}` : '');
|
||||
const getOffset = () => (sb.offset ? `OFFSET ${sb.offset}` : '');
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function createLogger({ name }: { name: string }): ILogger {
|
||||
HyperDX.getWinstonTransport(logLevel, {
|
||||
detectResources: true,
|
||||
service,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createLogger({ name }: { name: string }): ILogger {
|
||||
// Add ISO levels of logging from PINO
|
||||
levels: Object.assign(
|
||||
{ fatal: 0, warn: 4, trace: 7 },
|
||||
winston.config.syslog.levels
|
||||
winston.config.syslog.levels,
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -12,20 +10,9 @@
|
||||
"winston": "^3.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"date-fns": "^3.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -14,19 +12,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/sdk": "workspace:*",
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { Queue } from 'bullmq';
|
||||
export async function findJobByPrefix<T>(
|
||||
queue: Queue<T, any, string>,
|
||||
keys: string[],
|
||||
matcher: string
|
||||
matcher: string,
|
||||
) {
|
||||
const getTime = (val?: string) => {
|
||||
if (!val) return null;
|
||||
const match = val.match(/:(\d+)$/);
|
||||
return match?.[1] ? parseInt(match[1], 10) : null;
|
||||
return match?.[1] ? Number.parseInt(match[1], 10) : null;
|
||||
};
|
||||
const filtered = keys
|
||||
.filter((key) => key.includes(matcher))
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getRedisCache } from './redis';
|
||||
|
||||
export function cacheable<T extends (...args: any) => any>(
|
||||
fn: T,
|
||||
expireInSec: number
|
||||
expireInSec: number,
|
||||
) {
|
||||
const cachePrefix = `cachable:${fn.name}`;
|
||||
function stringify(obj: unknown): string {
|
||||
@@ -14,7 +14,7 @@ export function cacheable<T extends (...args: any) => any>(
|
||||
if (typeof obj === 'function') return obj.toString();
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return '[' + obj.map(stringify).join(',') + ']';
|
||||
return `[${obj.map(stringify).join(',')}]`;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
@@ -29,9 +29,9 @@ export function cacheable<T extends (...args: any) => any>(
|
||||
}
|
||||
const getKey = (...args: Parameters<T>) =>
|
||||
`${cachePrefix}:${stringify(args)}`;
|
||||
const cachedFn = async function (
|
||||
const cachedFn = async (
|
||||
...args: Parameters<T>
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
): Promise<Awaited<ReturnType<T>>> => {
|
||||
// JSON.stringify here is not bullet proof since ordering of object keys matters etc
|
||||
const key = getKey(...args);
|
||||
const cached = await getRedisCache().get(key);
|
||||
@@ -52,7 +52,7 @@ export function cacheable<T extends (...args: any) => any>(
|
||||
};
|
||||
|
||||
cachedFn.getKey = getKey;
|
||||
cachedFn.clear = async function (...args: Parameters<T>) {
|
||||
cachedFn.clear = async (...args: Parameters<T>) => {
|
||||
const key = getKey(...args);
|
||||
return getRedisCache().del(key);
|
||||
};
|
||||
|
||||
@@ -3,29 +3,15 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"ioredis": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export { Redis };
|
||||
|
||||
const createRedisClient = (
|
||||
url: string,
|
||||
overrides: RedisOptions = {}
|
||||
overrides: RedisOptions = {},
|
||||
): Redis => {
|
||||
const client = new Redis(url, { ...options, ...overrides });
|
||||
|
||||
@@ -58,7 +58,7 @@ export function getRedisQueue() {
|
||||
enableReadyCheck: false,
|
||||
maxRetriesPerRequest: null,
|
||||
enableOfflineQueue: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,11 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { OpenPanel } from '@openpanel/sdk';
|
||||
export * from '@openpanel/sdk';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
op: OpenPanel;
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -16,21 +14,10 @@
|
||||
"express": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/request-ip": "^0.0.41",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Script from 'next/script';
|
||||
import React from 'react';
|
||||
|
||||
import type {
|
||||
DecrementPayload,
|
||||
@@ -100,7 +100,7 @@ export function useOpenPanel() {
|
||||
}
|
||||
|
||||
function track(name: string, properties?: TrackProperties) {
|
||||
window.op && window.op('track', name, properties);
|
||||
window.op?.('track', name, properties);
|
||||
}
|
||||
|
||||
function screenView(properties: TrackProperties) {
|
||||
@@ -108,11 +108,11 @@ function screenView(properties: TrackProperties) {
|
||||
}
|
||||
|
||||
function identify(payload: IdentifyPayload) {
|
||||
window.op && window.op('identify', payload);
|
||||
window.op?.('identify', payload);
|
||||
}
|
||||
|
||||
function increment(payload: IncrementPayload) {
|
||||
window.op && window.op('increment', payload);
|
||||
window.op?.('increment', payload);
|
||||
}
|
||||
|
||||
function decrement(payload: DecrementPayload) {
|
||||
@@ -120,5 +120,5 @@ function decrement(payload: DecrementPayload) {
|
||||
}
|
||||
|
||||
function clear() {
|
||||
window.op && window.op('clear');
|
||||
window.op?.('clear');
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -17,20 +15,9 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.2.20",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppState, Platform } from 'react-native';
|
||||
import * as Application from 'expo-application';
|
||||
import Constants from 'expo-constants';
|
||||
import { AppState, Platform } from 'react-native';
|
||||
|
||||
import type { OpenPanelOptions, TrackProperties } from '@openpanel/sdk';
|
||||
import { OpenPanel as OpenPanelBase } from '@openpanel/sdk';
|
||||
|
||||
@@ -4,20 +4,14 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "1.0.0-local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.14.12",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
@@ -25,12 +19,5 @@
|
||||
"expo-application": "^5",
|
||||
"expo-constants": "14 - 16",
|
||||
"react-native": "0.73 - 0.74"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
|
||||
@@ -4,26 +4,13 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.14.12",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class Api {
|
||||
url: string,
|
||||
data: ReqBody,
|
||||
options: FetchOptions,
|
||||
attempt: number
|
||||
attempt: number,
|
||||
): Promise<ResBody | null> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
@@ -65,7 +65,7 @@ export class Api {
|
||||
return responseText ? JSON.parse(responseText) : null;
|
||||
} catch (error) {
|
||||
if (attempt < this.maxRetries) {
|
||||
const delay = this.initialRetryDelay * Math.pow(2, attempt);
|
||||
const delay = this.initialRetryDelay * 2 ** attempt;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.post<ReqBody, ResBody>(url, data, options, attempt + 1);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class Api {
|
||||
async fetch<ReqBody, ResBody>(
|
||||
path: string,
|
||||
data: ReqBody,
|
||||
options: FetchOptions = {}
|
||||
options: FetchOptions = {},
|
||||
): Promise<ResBody | null> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
return this.post<ReqBody, ResBody>(url, data, options, 0);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig(config as any);
|
||||
|
||||
@@ -4,28 +4,15 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "1.0.0-local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.14.12",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
import type {
|
||||
OpenPanelOptions as OpenPanelBaseOptions,
|
||||
TrackProperties,
|
||||
@@ -18,7 +16,7 @@ export type OpenPanelOptions = OpenPanelBaseOptions & {
|
||||
|
||||
function toCamelCase(str: string) {
|
||||
return str.replace(/([-_][a-z])/gi, ($1) =>
|
||||
$1.toUpperCase().replace('-', '').replace('_', '')
|
||||
$1.toUpperCase().replace('-', '').replace('_', ''),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +106,7 @@ export class OpenPanel extends OpenPanelBase {
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', function () {
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
|
||||
@@ -155,7 +153,7 @@ export class OpenPanel extends OpenPanelBase {
|
||||
screenView(path: string, properties?: TrackProperties): void;
|
||||
screenView(
|
||||
pathOrProperties?: string | TrackProperties,
|
||||
propertiesOrUndefined?: TrackProperties
|
||||
propertiesOrUndefined?: TrackProperties,
|
||||
): void {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' };
|
||||
import config from '@openpanel/tsconfig/tsup.config.json' assert {
|
||||
type: 'json',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -27,22 +25,11 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"@types/sqlstring": "^2.3.2",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function getProjectAccess({
|
||||
|
||||
export const getOrganizationAccessCached = cacheable(
|
||||
getOrganizationAccess,
|
||||
60 * 5
|
||||
60 * 5,
|
||||
);
|
||||
export async function getOrganizationAccess({
|
||||
userId,
|
||||
|
||||
@@ -29,13 +29,13 @@ import {
|
||||
import type { ISerieDataItem } from '@openpanel/common';
|
||||
import { alphabetIds } from '@openpanel/constants';
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
chQuery,
|
||||
createSqlBuilder,
|
||||
formatClickhouseDate,
|
||||
getChartSql,
|
||||
getEventFiltersWhereClause,
|
||||
getProfiles,
|
||||
TABLE_NAMES,
|
||||
} from '@openpanel/db';
|
||||
import type {
|
||||
FinalChart,
|
||||
@@ -53,7 +53,7 @@ function getEventLegend(event: IChartEvent) {
|
||||
|
||||
export function withFormula(
|
||||
{ formula, events }: IChartInput,
|
||||
series: Awaited<ReturnType<typeof getChartSerie>>
|
||||
series: Awaited<ReturnType<typeof getChartSerie>>,
|
||||
) {
|
||||
if (!formula) {
|
||||
return series;
|
||||
@@ -131,7 +131,7 @@ export function withFormula(
|
||||
const toDynamicISODateWithTZ = (
|
||||
date: string,
|
||||
blueprint: string,
|
||||
interval: IInterval
|
||||
interval: IInterval,
|
||||
) => {
|
||||
// If we have a space in the date we know it's a date with time
|
||||
if (date.includes(' ')) {
|
||||
@@ -324,8 +324,8 @@ export async function getFunnelData({
|
||||
repeat({}, diff - 1).map((_, index) => ({
|
||||
count: acc[acc.length - 1]?.count ?? 0,
|
||||
level: item.level + index + 1,
|
||||
}))
|
||||
)
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ export async function getFunnelData({
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as typeof funnelRes
|
||||
[] as typeof funnelRes,
|
||||
);
|
||||
|
||||
const totalSessions = last(filledFunnelRes)?.count ?? 0;
|
||||
@@ -367,7 +367,7 @@ export async function getFunnelData({
|
||||
dropoffCount: number;
|
||||
dropoffPercent: number;
|
||||
previousCount: number;
|
||||
}[]
|
||||
}[],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -432,7 +432,7 @@ export async function getFunnelStep({
|
||||
|
||||
return getProfiles(
|
||||
res.map((r) => r.id),
|
||||
projectId
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
||||
getChartSql({
|
||||
...payload,
|
||||
breakdowns: [],
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
@@ -452,7 +452,7 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
||||
|
||||
return getSeries()
|
||||
.then((data) =>
|
||||
completeSerie(data, payload.startDate, payload.endDate, payload.interval)
|
||||
completeSerie(data, payload.startDate, payload.endDate, payload.interval),
|
||||
)
|
||||
.then((series) => {
|
||||
return Object.keys(series).map((key) => {
|
||||
@@ -470,7 +470,7 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
||||
date: toDynamicISODateWithTZ(
|
||||
item.date,
|
||||
payload.startDate,
|
||||
payload.interval
|
||||
payload.interval,
|
||||
),
|
||||
})),
|
||||
};
|
||||
@@ -486,8 +486,8 @@ export async function getChartSeries(input: IChartInputWithDates) {
|
||||
getChartSerie({
|
||||
...input,
|
||||
event,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
|
||||
@@ -512,7 +512,7 @@ export async function getChart(input: IChartInput) {
|
||||
getChartSeries({
|
||||
...input,
|
||||
...previousPeriod,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -529,11 +529,11 @@ export async function getChart(input: IChartInput) {
|
||||
const final: FinalChart = {
|
||||
series: series.map((serie, index) => {
|
||||
const eventIndex = input.events.findIndex(
|
||||
(event) => event.id === serie.event.id
|
||||
(event) => event.id === serie.event.id,
|
||||
);
|
||||
const alphaId = alphabetIds[eventIndex];
|
||||
const previousSerie = previousSeries?.find(
|
||||
(prevSerie) => getSerieId(prevSerie) === getSerieId(serie)
|
||||
(prevSerie) => getSerieId(prevSerie) === getSerieId(serie),
|
||||
);
|
||||
const metrics = {
|
||||
sum: sum(serie.data.map((item) => item.count)),
|
||||
@@ -561,30 +561,30 @@ export async function getChart(input: IChartInput) {
|
||||
metrics.sum,
|
||||
previousSerie
|
||||
? sum(previousSerie?.data.map((item) => item.count))
|
||||
: null
|
||||
: null,
|
||||
),
|
||||
average: getPreviousMetric(
|
||||
metrics.average,
|
||||
previousSerie
|
||||
? round(
|
||||
average(
|
||||
previousSerie?.data.map((item) => item.count)
|
||||
previousSerie?.data.map((item) => item.count),
|
||||
),
|
||||
2
|
||||
2,
|
||||
)
|
||||
: null
|
||||
: null,
|
||||
),
|
||||
min: getPreviousMetric(
|
||||
metrics.sum,
|
||||
previousSerie
|
||||
? min(previousSerie?.data.map((item) => item.count))
|
||||
: null
|
||||
: null,
|
||||
),
|
||||
max: getPreviousMetric(
|
||||
metrics.sum,
|
||||
previousSerie
|
||||
? max(previousSerie?.data.map((item) => item.count))
|
||||
: null
|
||||
: null,
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -596,7 +596,7 @@ export async function getChart(input: IChartInput) {
|
||||
previous: previousSerie?.data[index]
|
||||
? getPreviousMetric(
|
||||
item.count ?? 0,
|
||||
previousSerie?.data[index]?.count ?? null
|
||||
previousSerie?.data[index]?.count ?? null,
|
||||
)
|
||||
: undefined,
|
||||
})),
|
||||
@@ -617,16 +617,15 @@ export async function getChart(input: IChartInput) {
|
||||
const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
|
||||
const sumB = b.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
|
||||
return sumB - sumA;
|
||||
} else {
|
||||
return b.metrics[input.metric] - a.metrics[input.metric];
|
||||
}
|
||||
return b.metrics[input.metric] - a.metrics[input.metric];
|
||||
})
|
||||
.slice(offset, limit ? offset + limit : series.length);
|
||||
|
||||
final.metrics.sum = sum(final.series.map((item) => item.metrics.sum));
|
||||
final.metrics.average = round(
|
||||
average(final.series.map((item) => item.metrics.average)),
|
||||
2
|
||||
2,
|
||||
);
|
||||
final.metrics.min = min(final.series.map((item) => item.metrics.min));
|
||||
final.metrics.max = max(final.series.map((item) => item.metrics.max));
|
||||
@@ -634,26 +633,26 @@ export async function getChart(input: IChartInput) {
|
||||
final.metrics.previous = {
|
||||
sum: getPreviousMetric(
|
||||
final.metrics.sum,
|
||||
sum(final.series.map((item) => item.metrics.previous?.sum?.value ?? 0))
|
||||
sum(final.series.map((item) => item.metrics.previous?.sum?.value ?? 0)),
|
||||
),
|
||||
average: getPreviousMetric(
|
||||
final.metrics.average,
|
||||
round(
|
||||
average(
|
||||
final.series.map(
|
||||
(item) => item.metrics.previous?.average?.value ?? 0
|
||||
)
|
||||
(item) => item.metrics.previous?.average?.value ?? 0,
|
||||
),
|
||||
),
|
||||
2
|
||||
)
|
||||
2,
|
||||
),
|
||||
),
|
||||
min: getPreviousMetric(
|
||||
final.metrics.min,
|
||||
min(final.series.map((item) => item.metrics.previous?.min?.value ?? 0))
|
||||
min(final.series.map((item) => item.metrics.previous?.min?.value ?? 0)),
|
||||
),
|
||||
max: getPreviousMetric(
|
||||
final.metrics.max,
|
||||
max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0))
|
||||
max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { escape } from 'sqlstring';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
chQuery,
|
||||
createSqlBuilder,
|
||||
db,
|
||||
formatClickhouseDate,
|
||||
getSelectPropertyKey,
|
||||
TABLE_NAMES,
|
||||
toDate,
|
||||
} from '@openpanel/db';
|
||||
import { zChartInput, zRange, zTimeInterval } from '@openpanel/validation';
|
||||
@@ -33,12 +33,12 @@ export const chartRouter = createTRPCRouter({
|
||||
interval: zTimeInterval,
|
||||
startDate: z.string().nullish(),
|
||||
endDate: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, ...input } }) => {
|
||||
const { startDate, endDate } = getChartStartEndDate(input);
|
||||
const events = await chQuery<{ name: string }>(
|
||||
`SELECT DISTINCT name FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND ${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`
|
||||
`SELECT DISTINCT name FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND ${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`,
|
||||
);
|
||||
|
||||
return [
|
||||
@@ -58,7 +58,7 @@ export const chartRouter = createTRPCRouter({
|
||||
interval: zTimeInterval,
|
||||
startDate: z.string().nullish(),
|
||||
endDate: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, event, ...input } }) => {
|
||||
const { startDate, endDate } = getChartStartEndDate(input);
|
||||
@@ -66,7 +66,7 @@ export const chartRouter = createTRPCRouter({
|
||||
`SELECT distinct mapKeys(properties) as keys from ${TABLE_NAMES.events} where ${
|
||||
event && event !== '*' ? `name = ${escape(event)} AND ` : ''
|
||||
} project_id = ${escape(projectId)} AND
|
||||
${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`
|
||||
${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`,
|
||||
);
|
||||
|
||||
const properties = events
|
||||
@@ -93,12 +93,12 @@ export const chartRouter = createTRPCRouter({
|
||||
'browser_version',
|
||||
'device',
|
||||
'brand',
|
||||
'model'
|
||||
'model',
|
||||
);
|
||||
|
||||
return pipe(
|
||||
sort<string>((a, b) => a.length - b.length),
|
||||
uniq
|
||||
uniq,
|
||||
)(properties);
|
||||
}),
|
||||
|
||||
@@ -112,7 +112,7 @@ export const chartRouter = createTRPCRouter({
|
||||
interval: zTimeInterval,
|
||||
startDate: z.string().nullish(),
|
||||
endDate: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { event, property, projectId, ...input } }) => {
|
||||
const { startDate, endDate } = getChartStartEndDate(input);
|
||||
@@ -136,7 +136,7 @@ export const chartRouter = createTRPCRouter({
|
||||
(data: typeof events) => map(prop('values'), data),
|
||||
flatten,
|
||||
uniq,
|
||||
sort((a, b) => a.length - b.length)
|
||||
sort((a, b) => a.length - b.length),
|
||||
)(events);
|
||||
|
||||
return {
|
||||
@@ -166,7 +166,7 @@ export const chartRouter = createTRPCRouter({
|
||||
.input(
|
||||
zChartInput.extend({
|
||||
step: z.number(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const currentPeriod = getChartStartEndDate(input);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import crypto from 'crypto';
|
||||
import crypto from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||
import { stripTrailingSlash } from '@openpanel/common';
|
||||
import type { Prisma } from '@openpanel/db';
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
import { hashPassword } from '@openpanel/common/server';
|
||||
import { getClientAccess } from '../access';
|
||||
import { TRPCAccessError } from '../errors';
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
@@ -17,7 +18,7 @@ export const clientRouter = createTRPCRouter({
|
||||
name: z.string(),
|
||||
cors: z.string().nullable(),
|
||||
crossDomain: z.boolean().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getClientAccess({
|
||||
@@ -49,7 +50,7 @@ export const clientRouter = createTRPCRouter({
|
||||
cors: z.string().nullable(),
|
||||
crossDomain: z.boolean().optional(),
|
||||
type: z.enum(['read', 'write', 'root']).optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const secret = `sec_${crypto.randomBytes(10).toString('hex')}`;
|
||||
@@ -75,7 +76,7 @@ export const clientRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getClientAccess({
|
||||
|
||||
@@ -18,7 +18,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(({ input }) => {
|
||||
return getDashboardsByProjectId(input.projectId);
|
||||
@@ -28,7 +28,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getProjectAccess({
|
||||
@@ -61,7 +61,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const dashboard = await db.dashboard.findUniqueOrThrow({
|
||||
@@ -93,7 +93,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
forceDelete: z.boolean().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const dashboard = await db.dashboard.findUniqueOrThrow({
|
||||
@@ -132,7 +132,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
switch (error.code) {
|
||||
case PrismaError.ForeignConstraintViolation:
|
||||
throw new Error(
|
||||
'Cannot delete dashboard with associated reports'
|
||||
'Cannot delete dashboard with associated reports',
|
||||
);
|
||||
default:
|
||||
throw new Error('Unknown error deleting dashboard');
|
||||
|
||||
@@ -3,13 +3,13 @@ import { escape } from 'sqlstring';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
chQuery,
|
||||
convertClickhouseDateToJs,
|
||||
db,
|
||||
getEventList,
|
||||
getEvents,
|
||||
getTopPages,
|
||||
TABLE_NAMES,
|
||||
} from '@openpanel/db';
|
||||
import { zChartEventFilter } from '@openpanel/validation';
|
||||
|
||||
@@ -26,7 +26,7 @@ export const eventRouter = createTRPCRouter({
|
||||
icon: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
conversion: z.boolean().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(
|
||||
async ({ input: { projectId, name, icon, color, conversion } }) => {
|
||||
@@ -40,7 +40,7 @@ export const eventRouter = createTRPCRouter({
|
||||
create: { projectId, name, icon, color, conversion },
|
||||
update: { icon, color, conversion },
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
byId: protectedProcedure
|
||||
@@ -48,14 +48,14 @@ export const eventRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { id, projectId } }) => {
|
||||
const res = await getEvents(
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE id = ${escape(id)} AND project_id = ${escape(projectId)};`,
|
||||
{
|
||||
meta: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!res?.[0]) {
|
||||
@@ -81,7 +81,7 @@ export const eventRouter = createTRPCRouter({
|
||||
endDate: z.date().optional(),
|
||||
meta: z.boolean().optional(),
|
||||
profile: z.boolean().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return getEventList(input);
|
||||
@@ -90,7 +90,7 @@ export const eventRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId } }) => {
|
||||
const conversions = await db.eventMeta.findMany({
|
||||
@@ -109,7 +109,7 @@ export const eventRouter = createTRPCRouter({
|
||||
{
|
||||
profile: true,
|
||||
meta: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -119,7 +119,7 @@ export const eventRouter = createTRPCRouter({
|
||||
projectId: z.string(),
|
||||
cursor: z.number().optional(),
|
||||
limit: z.number().default(8),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, cursor, limit }, ctx }) => {
|
||||
if (ctx.session.userId) {
|
||||
@@ -151,12 +151,12 @@ export const eventRouter = createTRPCRouter({
|
||||
path: string;
|
||||
created_at: string;
|
||||
}>(
|
||||
`SELECT * FROM ${TABLE_NAMES.events_bots} WHERE project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${(cursor ?? 0) * limit}`
|
||||
`SELECT * FROM ${TABLE_NAMES.events_bots} WHERE project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${(cursor ?? 0) * limit}`,
|
||||
),
|
||||
chQuery<{
|
||||
count: number;
|
||||
}>(
|
||||
`SELECT count(*) as count FROM ${TABLE_NAMES.events_bots} WHERE project_id = ${escape(projectId)}`
|
||||
`SELECT count(*) as count FROM ${TABLE_NAMES.events_bots} WHERE project_id = ${escape(projectId)}`,
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -176,7 +176,7 @@ export const eventRouter = createTRPCRouter({
|
||||
cursor: z.number().optional(),
|
||||
take: z.number().default(20),
|
||||
search: z.string().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return getTopPages(input);
|
||||
@@ -186,19 +186,19 @@ export const eventRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const res = await chQuery<{ origin: string }>(
|
||||
`SELECT DISTINCT origin FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(
|
||||
input.projectId
|
||||
)} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY ORDER BY origin ASC`
|
||||
input.projectId,
|
||||
)} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY ORDER BY origin ASC`,
|
||||
);
|
||||
|
||||
return res.sort((a, b) =>
|
||||
a.origin
|
||||
.replace(/https?:\/\//, '')
|
||||
.localeCompare(b.origin.replace(/https?:\/\//, ''))
|
||||
.localeCompare(b.origin.replace(/https?:\/\//, '')),
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import crypto from 'crypto';
|
||||
import crypto from 'node:crypto';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||
import { stripTrailingSlash } from '@openpanel/common';
|
||||
import { db, getId, getOrganizationBySlug, getUserById } from '@openpanel/db';
|
||||
import type { ProjectType } from '@openpanel/db';
|
||||
import { zOnboardingProject } from '@openpanel/validation';
|
||||
|
||||
import { hashPassword } from '@openpanel/common/server';
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
|
||||
async function createOrGetOrganization(
|
||||
input: z.infer<typeof zOnboardingProject>,
|
||||
userId: string
|
||||
userId: string,
|
||||
) {
|
||||
if (input.organizationSlug) {
|
||||
return await getOrganizationBySlug(input.organizationSlug);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getOrganizationAccess({
|
||||
@@ -86,7 +86,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const member = await db.member.findUniqueOrThrow({
|
||||
@@ -107,7 +107,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
const invitationId = pathOr<string | undefined>(
|
||||
undefined,
|
||||
['meta', 'invitationId'],
|
||||
member
|
||||
member,
|
||||
);
|
||||
|
||||
if (invitationId) {
|
||||
@@ -130,7 +130,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
userId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.session.userId === input.userId) {
|
||||
@@ -168,7 +168,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
userId: z.string(),
|
||||
organizationSlug: z.string(),
|
||||
access: z.array(z.string()),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getOrganizationAccess({
|
||||
|
||||
@@ -3,11 +3,11 @@ import { escape } from 'sqlstring';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
chQuery,
|
||||
createSqlBuilder,
|
||||
getProfileList,
|
||||
getProfiles,
|
||||
TABLE_NAMES,
|
||||
} from '@openpanel/db';
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
@@ -17,7 +17,7 @@ export const profileRouter = createTRPCRouter({
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input: { projectId } }) => {
|
||||
const events = await chQuery<{ keys: string[] }>(
|
||||
`SELECT distinct mapKeys(properties) as keys from ${TABLE_NAMES.profiles} where project_id = ${escape(projectId)};`
|
||||
`SELECT distinct mapKeys(properties) as keys from ${TABLE_NAMES.profiles} where project_id = ${escape(projectId)};`,
|
||||
);
|
||||
|
||||
const properties = events
|
||||
@@ -30,7 +30,7 @@ export const profileRouter = createTRPCRouter({
|
||||
|
||||
return pipe(
|
||||
sort<string>((a, b) => a.length - b.length),
|
||||
uniq
|
||||
uniq,
|
||||
)(properties);
|
||||
}),
|
||||
|
||||
@@ -42,7 +42,7 @@ export const profileRouter = createTRPCRouter({
|
||||
take: z.number().default(50),
|
||||
search: z.string().optional(),
|
||||
// filters: z.array(zChartEventFilter).default([]),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, cursor, take, search } }) => {
|
||||
return getProfileList({ projectId, cursor, take, search });
|
||||
@@ -55,15 +55,15 @@ export const profileRouter = createTRPCRouter({
|
||||
cursor: z.number().optional(),
|
||||
take: z.number().default(50),
|
||||
// filters: z.array(zChartEventFilter).default([]),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, cursor, take } }) => {
|
||||
const res = await chQuery<{ profile_id: string; count: number }>(
|
||||
`SELECT profile_id, count(*) as count from ${TABLE_NAMES.events} where profile_id != '' and project_id = ${escape(projectId)} group by profile_id order by count() DESC LIMIT ${take} ${cursor ? `OFFSET ${cursor * take}` : ''}`
|
||||
`SELECT profile_id, count(*) as count from ${TABLE_NAMES.events} where profile_id != '' and project_id = ${escape(projectId)} group by profile_id order by count() DESC LIMIT ${take} ${cursor ? `OFFSET ${cursor * take}` : ''}`,
|
||||
);
|
||||
const profiles = await getProfiles(
|
||||
res.map((r) => r.profile_id),
|
||||
projectId
|
||||
projectId,
|
||||
);
|
||||
return (
|
||||
res
|
||||
@@ -83,7 +83,7 @@ export const profileRouter = createTRPCRouter({
|
||||
z.object({
|
||||
property: z.string(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { property, projectId } }) => {
|
||||
const { sb, getSql } = createSqlBuilder();
|
||||
@@ -91,7 +91,7 @@ export const profileRouter = createTRPCRouter({
|
||||
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||
if (property.startsWith('properties.')) {
|
||||
sb.select.values = `distinct arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.'),
|
||||
)}))) as values`;
|
||||
} else {
|
||||
sb.select.values = `${property} as values`;
|
||||
@@ -103,7 +103,7 @@ export const profileRouter = createTRPCRouter({
|
||||
(data: typeof profiles) => map(prop('values'), data),
|
||||
flatten,
|
||||
uniq,
|
||||
sort((a, b) => a.length - b.length)
|
||||
sort((a, b) => a.length - b.length),
|
||||
)(profiles);
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
organizationSlug: z.string().nullable(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { organizationSlug } }) => {
|
||||
if (organizationSlug === null) return [];
|
||||
@@ -23,7 +23,7 @@ export const projectRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getProjectAccess({
|
||||
@@ -49,7 +49,7 @@ export const projectRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
organizationSlug: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { name, organizationSlug } }) => {
|
||||
return db.project.create({
|
||||
@@ -65,7 +65,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const access = await getProjectAccess({
|
||||
|
||||
@@ -21,7 +21,7 @@ export const referenceRouter = createTRPCRouter({
|
||||
date: new Date(datetime),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
delete: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
@@ -54,7 +54,7 @@ export const referenceRouter = createTRPCRouter({
|
||||
startDate: z.string().nullish(),
|
||||
endDate: z.string().nullish(),
|
||||
range: zRange,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.query(({ input: { projectId, ...input } }) => {
|
||||
const { startDate, endDate } = getChartStartEndDate(input);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const reportRouter = createTRPCRouter({
|
||||
z.object({
|
||||
report: zReportInput.omit({ projectId: true }),
|
||||
dashboardId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { report, dashboardId }, ctx }) => {
|
||||
const dashboard = await db.dashboard.findUniqueOrThrow({
|
||||
@@ -52,7 +52,7 @@ export const reportRouter = createTRPCRouter({
|
||||
z.object({
|
||||
reportId: z.string(),
|
||||
report: zReportInput.omit({ projectId: true }),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { report, reportId }, ctx }) => {
|
||||
const dbReport = await db.report.findUniqueOrThrow({
|
||||
@@ -91,7 +91,7 @@ export const reportRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
reportId: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { reportId }, ctx }) => {
|
||||
const report = await db.report.findUniqueOrThrow({
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ticketRouter = createTRPCRouter({
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
meta: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!API_KEY) {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const userRouter = createTRPCRouter({
|
||||
z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const [updatedUser] = await Promise.all([
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAuth } from '@clerk/fastify';
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import { TRPCError, initTRPC } from '@trpc/server';
|
||||
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
||||
import { has } from 'ramda';
|
||||
import superjson from 'superjson';
|
||||
@@ -21,10 +21,9 @@ export function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||
options: {
|
||||
maxAge: number;
|
||||
path: string;
|
||||
}
|
||||
},
|
||||
) => {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
res.setCookie(key, value, options);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -12,20 +10,9 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/eslint-config": "workspace:*",
|
||||
"@openpanel/prettier-config": "workspace:*",
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@openpanel/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@openpanel/constants';
|
||||
|
||||
export function objectToZodEnums<K extends string>(
|
||||
obj: Record<K, any>
|
||||
obj: Record<K, any>,
|
||||
): [K, ...K[]] {
|
||||
const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
|
||||
return [firstKey!, ...otherKeys];
|
||||
@@ -139,12 +139,12 @@ export const zOnboardingProject = z
|
||||
data.app === false &&
|
||||
data.backend === false
|
||||
) {
|
||||
['app', 'backend', 'website'].forEach((key) => {
|
||||
for (const key of ['app', 'backend', 'website']) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'At least one type must be selected',
|
||||
path: [key],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user