- {name}
+ {name}
diff --git a/apps/web/src/hooks/useCursor.ts b/apps/web/src/hooks/useCursor.ts
new file mode 100644
index 00000000..d0037913
--- /dev/null
+++ b/apps/web/src/hooks/useCursor.ts
@@ -0,0 +1,12 @@
+import { parseAsIsoDateTime, useQueryState } from 'nuqs';
+
+export function useCursor() {
+ const [cursor, setCursor] = useQueryState(
+ 'cursor',
+ parseAsIsoDateTime.withOptions({ shallow: false })
+ );
+ return {
+ cursor,
+ setCursor,
+ };
+}
diff --git a/apps/web/src/hooks/useEventQueryFilters copy.ts b/apps/web/src/hooks/useEventQueryFilters copy.ts
new file mode 100644
index 00000000..ab33da44
--- /dev/null
+++ b/apps/web/src/hooks/useEventQueryFilters copy.ts
@@ -0,0 +1,311 @@
+import { useMemo } from 'react';
+import type { IChartInput } from '@/types';
+import { parseAsString, useQueryState } from 'nuqs';
+
+const nuqsOptions = { history: 'push' } as const;
+
+export function useEventQueryFiltersqweqweqweqweqwe() {
+ // Path
+ const [path, setPath] = useQueryState(
+ 'path',
+ parseAsString.withOptions(nuqsOptions)
+ );
+
+ // Referrer
+ const [referrer, setReferrer] = useQueryState(
+ 'referrer',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [referrerName, setReferrerName] = useQueryState(
+ 'referrer_name',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [referrerType, setReferrerType] = useQueryState(
+ 'referrer_type',
+ parseAsString.withOptions(nuqsOptions)
+ );
+
+ // Sources
+ const [utmSource, setUtmSource] = useQueryState(
+ 'utm_source',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [utmMedium, setUtmMedium] = useQueryState(
+ 'utm_medium',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [utmCampaign, setUtmCampaign] = useQueryState(
+ 'utm_campaign',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [utmContent, setUtmContent] = useQueryState(
+ 'utm_content',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [utmTerm, setUtmTerm] = useQueryState(
+ 'utm_term',
+ parseAsString.withOptions(nuqsOptions)
+ );
+
+ // Geo
+ const [country, setCountry] = useQueryState(
+ 'country',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [region, setRegion] = useQueryState(
+ 'region',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [city, setCity] = useQueryState(
+ 'city',
+ parseAsString.withOptions(nuqsOptions)
+ );
+
+ // tech
+ const [device, setDevice] = useQueryState(
+ 'device',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [browser, setBrowser] = useQueryState(
+ 'browser',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [browserVersion, setBrowserVersion] = useQueryState(
+ 'browser_version',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [os, setOS] = useQueryState(
+ 'os',
+ parseAsString.withOptions(nuqsOptions)
+ );
+ const [osVersion, setOSVersion] = useQueryState(
+ 'os_version',
+ parseAsString.withOptions(nuqsOptions)
+ );
+
+ const filters = useMemo(() => {
+ const filters: IChartInput['events'][number]['filters'] = [];
+
+ if (path) {
+ filters.push({
+ id: 'path',
+ operator: 'is',
+ name: 'path',
+ value: [path],
+ });
+ }
+
+ if (device) {
+ filters.push({
+ id: 'device',
+ operator: 'is',
+ name: 'device',
+ value: [device],
+ });
+ }
+
+ if (referrer) {
+ filters.push({
+ id: 'referrer',
+ operator: 'is',
+ name: 'referrer',
+ value: [referrer],
+ });
+ }
+
+ if (referrerName) {
+ filters.push({
+ id: 'referrer_name',
+ operator: 'is',
+ name: 'referrer_name',
+ value: [referrerName],
+ });
+ }
+
+ if (referrerType) {
+ filters.push({
+ id: 'referrer_type',
+ operator: 'is',
+ name: 'referrer_type',
+ value: [referrerType],
+ });
+ }
+
+ if (utmSource) {
+ filters.push({
+ id: 'utm_source',
+ operator: 'is',
+ name: 'properties.query.utm_source',
+ value: [utmSource],
+ });
+ }
+
+ if (utmMedium) {
+ filters.push({
+ id: 'utm_medium',
+ operator: 'is',
+ name: 'properties.query.utm_medium',
+ value: [utmMedium],
+ });
+ }
+
+ if (utmCampaign) {
+ filters.push({
+ id: 'utm_campaign',
+ operator: 'is',
+ name: 'properties.query.utm_campaign',
+ value: [utmCampaign],
+ });
+ }
+
+ if (utmContent) {
+ filters.push({
+ id: 'utm_content',
+ operator: 'is',
+ name: 'properties.query.utm_content',
+ value: [utmContent],
+ });
+ }
+
+ if (utmTerm) {
+ filters.push({
+ id: 'utm_term',
+ operator: 'is',
+ name: 'properties.query.utm_term',
+ value: [utmTerm],
+ });
+ }
+
+ if (country) {
+ filters.push({
+ id: 'country',
+ operator: 'is',
+ name: 'country',
+ value: [country],
+ });
+ }
+
+ if (region) {
+ filters.push({
+ id: 'region',
+ operator: 'is',
+ name: 'region',
+ value: [region],
+ });
+ }
+
+ if (city) {
+ filters.push({
+ id: 'city',
+ operator: 'is',
+ name: 'city',
+ value: [city],
+ });
+ }
+
+ if (browser) {
+ filters.push({
+ id: 'browser',
+ operator: 'is',
+ name: 'browser',
+ value: [browser],
+ });
+ }
+
+ if (browserVersion) {
+ filters.push({
+ id: 'browser_version',
+ operator: 'is',
+ name: 'browser_version',
+ value: [browserVersion],
+ });
+ }
+
+ if (os) {
+ filters.push({
+ id: 'os',
+ operator: 'is',
+ name: 'os',
+ value: [os],
+ });
+ }
+
+ if (osVersion) {
+ filters.push({
+ id: 'os_version',
+ operator: 'is',
+ name: 'os_version',
+ value: [osVersion],
+ });
+ }
+
+ return filters;
+ }, [
+ path,
+ device,
+ referrer,
+ referrerName,
+ referrerType,
+ utmSource,
+ utmMedium,
+ utmCampaign,
+ utmContent,
+ utmTerm,
+ country,
+ region,
+ city,
+ browser,
+ browserVersion,
+ os,
+ osVersion,
+ ]);
+
+ return {
+ // Computed
+ filters,
+
+ // Path
+ path,
+ setPath,
+
+ // Refs
+ referrer,
+ setReferrer,
+ referrerName,
+ setReferrerName,
+ referrerType,
+ setReferrerType,
+
+ // UTM
+ utmSource,
+ setUtmSource,
+ utmMedium,
+ setUtmMedium,
+ utmCampaign,
+ setUtmCampaign,
+ utmContent,
+ setUtmContent,
+ utmTerm,
+ setUtmTerm,
+
+ // GEO
+ country,
+ setCountry,
+ region,
+ setRegion,
+ city,
+ setCity,
+
+ // Tech
+ device,
+ setDevice,
+ browser,
+ setBrowser,
+ browserVersion,
+ setBrowserVersion,
+ os,
+ setOS,
+ osVersion,
+ setOSVersion,
+ };
+}
diff --git a/apps/web/src/hooks/useEventQueryFilters.ts b/apps/web/src/hooks/useEventQueryFilters.ts
new file mode 100644
index 00000000..9a0e0add
--- /dev/null
+++ b/apps/web/src/hooks/useEventQueryFilters.ts
@@ -0,0 +1,227 @@
+import { useMemo } from 'react';
+import type { IChartInput } from '@/types';
+
+// prettier-ignore
+import type { UseQueryStateReturn } from 'nuqs';
+
+import { parseAsString, useQueryState } from 'nuqs';
+
+const nuqsOptions = { history: 'push' } as const;
+
+function useFix(hook: UseQueryStateReturn) {
+ return useMemo(
+ () => ({
+ get: hook[0],
+ set: hook[1],
+ }),
+ [hook]
+ );
+}
+
+export function useEventQueryFilters() {
+ // Ignore prettier so that we have all one same line
+ // prettier-ignore
+ return {
+ path: useFix(useQueryState('path', parseAsString.withOptions(nuqsOptions))),
+ referrer: useFix(useQueryState('referrer', parseAsString.withOptions(nuqsOptions))),
+ referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions(nuqsOptions))),
+ referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions(nuqsOptions))),
+ utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions(nuqsOptions))),
+ utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions(nuqsOptions))),
+ utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions(nuqsOptions))),
+ utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions(nuqsOptions))),
+ utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions(nuqsOptions))),
+ country: useFix(useQueryState('country', parseAsString.withOptions(nuqsOptions))),
+ region: useFix(useQueryState('region', parseAsString.withOptions(nuqsOptions))),
+ city: useFix(useQueryState('city', parseAsString.withOptions(nuqsOptions))),
+ device: useFix(useQueryState('device', parseAsString.withOptions(nuqsOptions))),
+ browser: useFix(useQueryState('browser', parseAsString.withOptions(nuqsOptions))),
+ browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions(nuqsOptions))),
+ os: useFix(useQueryState('os', parseAsString.withOptions(nuqsOptions))),
+ osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions(nuqsOptions))),
+ } as const;
+}
+
+export function useEventFilters() {
+ const hej = useEventQueryFilters();
+
+ const filters = useMemo(() => {
+ const filters: IChartInput['events'][number]['filters'] = [];
+
+ if (hej.path.get) {
+ filters.push({
+ id: 'path',
+ operator: 'is',
+ name: 'path' as const,
+ value: [hej.path.get],
+ });
+ }
+
+ if (hej.device.get) {
+ filters.push({
+ id: 'device',
+ operator: 'is',
+ name: 'device' as const,
+ value: [hej.device.get],
+ });
+ }
+
+ if (hej.referrer.get) {
+ filters.push({
+ id: 'referrer',
+ operator: 'is',
+ name: 'referrer' as const,
+ value: [hej.referrer.get],
+ });
+ }
+ console.log('hej.referrerName.get', hej.referrerName.get);
+
+ if (hej.referrerName.get) {
+ filters.push({
+ id: 'referrerName',
+ operator: 'is',
+ name: 'referrer_name' as const,
+ value: [hej.referrerName.get],
+ });
+ }
+
+ if (hej.referrerType.get) {
+ filters.push({
+ id: 'referrerType',
+ operator: 'is',
+ name: 'referrer_type' as const,
+ value: [hej.referrerType.get],
+ });
+ }
+
+ if (hej.utmSource.get) {
+ filters.push({
+ id: 'utmSource',
+ operator: 'is',
+ name: 'properties.query.utm_source' as const,
+ value: [hej.utmSource.get],
+ });
+ }
+
+ if (hej.utmMedium.get) {
+ filters.push({
+ id: 'utmMedium',
+ operator: 'is',
+ name: 'properties.query.utm_medium' as const,
+ value: [hej.utmMedium.get],
+ });
+ }
+
+ if (hej.utmCampaign.get) {
+ filters.push({
+ id: 'utmCampaign',
+ operator: 'is',
+ name: 'properties.query.utm_campaign' as const,
+ value: [hej.utmCampaign.get],
+ });
+ }
+
+ if (hej.utmContent.get) {
+ filters.push({
+ id: 'utmContent',
+ operator: 'is',
+ name: 'properties.query.utm_content' as const,
+ value: [hej.utmContent.get],
+ });
+ }
+
+ if (hej.utmTerm.get) {
+ filters.push({
+ id: 'utmTerm',
+ operator: 'is',
+ name: 'properties.query.utm_term' as const,
+ value: [hej.utmTerm.get],
+ });
+ }
+
+ if (hej.country.get) {
+ filters.push({
+ id: 'country',
+ operator: 'is',
+ name: 'country' as const,
+ value: [hej.country.get],
+ });
+ }
+
+ if (hej.region.get) {
+ filters.push({
+ id: 'region',
+ operator: 'is',
+ name: 'region' as const,
+ value: [hej.region.get],
+ });
+ }
+
+ if (hej.city.get) {
+ filters.push({
+ id: 'city',
+ operator: 'is',
+ name: 'city' as const,
+ value: [hej.city.get],
+ });
+ }
+
+ if (hej.browser.get) {
+ filters.push({
+ id: 'browser',
+ operator: 'is',
+ name: 'browser' as const,
+ value: [hej.browser.get],
+ });
+ }
+
+ if (hej.browserVersion.get) {
+ filters.push({
+ id: 'browserVersion',
+ operator: 'is',
+ name: 'browser_version' as const,
+ value: [hej.browserVersion.get],
+ });
+ }
+
+ if (hej.os.get) {
+ filters.push({
+ id: 'os',
+ operator: 'is',
+ name: 'os' as const,
+ value: [hej.os.get],
+ });
+ }
+
+ if (hej.osVersion.get) {
+ filters.push({
+ id: 'osVersion',
+ operator: 'is',
+ name: 'os_version' as const,
+ value: [hej.osVersion.get],
+ });
+ }
+
+ return filters;
+ }, [
+ hej.path,
+ hej.device,
+ hej.referrer,
+ hej.referrerName,
+ hej.referrerType,
+ hej.utmSource,
+ hej.utmMedium,
+ hej.utmCampaign,
+ hej.utmContent,
+ hej.utmTerm,
+ hej.country,
+ hej.region,
+ hej.city,
+ hej.browser,
+ hej.browserVersion,
+ hej.os,
+ hej.osVersion,
+ ]);
+
+ return filters;
+}
diff --git a/apps/web/src/utils/getters.ts b/apps/web/src/utils/getters.ts
index 53d3e784..838c5135 100644
--- a/apps/web/src/utils/getters.ts
+++ b/apps/web/src/utils/getters.ts
@@ -1,6 +1,6 @@
-import type { Profile } from '@mixan/db';
+import type { IDBProfile } from '@mixan/db';
-export function getProfileName(profile: Profile | undefined | null) {
+export function getProfileName(profile: IDBProfile | undefined | null) {
if (!profile) return 'No profile';
return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
}
diff --git a/packages/db/clickhouse_tables.sql b/packages/db/clickhouse_tables.sql
index d9f8162c..dc765520 100644
--- a/packages/db/clickhouse_tables.sql
+++ b/packages/db/clickhouse_tables.sql
@@ -1,4 +1,10 @@
+ALTER TABLE
+ events
+ADD
+ COLUMN id UUID;
+
CREATE TABLE openpanel.events (
+ `id` UUID,
`name` String,
`profile_id` String,
`project_id` String,
diff --git a/packages/db/package.json b/packages/db/package.json
index 3296fdac..b98c3f1c 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -12,11 +12,12 @@
"with-env": "dotenv -e ../../.env -c --"
},
"dependencies": {
+ "@clickhouse/client": "^0.2.9",
"@mixan/common": "workspace:*",
"@mixan/redis": "workspace:*",
- "@clickhouse/client": "^0.2.9",
"@prisma/client": "^5.1.1",
- "ramda": "^0.29.1"
+ "ramda": "^0.29.1",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@mixan/eslint-config": "workspace:*",
@@ -25,6 +26,7 @@
"@mixan/types": "workspace:*",
"@types/node": "^18.16.0",
"@types/ramda": "^0.29.6",
+ "@types/uuid": "^9.0.8",
"eslint": "^8.48.0",
"prettier": "^3.0.3",
"prisma": "^5.1.1",
diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts
index 1bd5b1f5..90c7b256 100644
--- a/packages/db/src/services/event.service.ts
+++ b/packages/db/src/services/event.service.ts
@@ -1,5 +1,5 @@
-import type { IDBProfile } from '@/prisma-types';
import { omit } from 'ramda';
+import { v4 as uuid } from 'uuid';
import { randomSplitName, toDots } from '@mixan/common';
import { redis, redisPub } from '@mixan/redis';
@@ -12,8 +12,11 @@ import {
} from '../clickhouse-client';
import type { Prisma } from '../prisma-client';
import { db } from '../prisma-client';
+import type { IDBProfile } from '../prisma-types';
+import { createSqlBuilder } from '../sql-builder';
export interface IClickhouseEvent {
+ id: string;
name: string;
profile_id: string;
project_id: string;
@@ -41,6 +44,7 @@ export function transformEvent(
event: IClickhouseEvent
): IServiceCreateEventPayload {
return {
+ id: event.id,
name: event.name,
profileId: event.profile_id,
projectId: event.project_id,
@@ -66,6 +70,7 @@ export function transformEvent(
}
export interface IServiceCreateEventPayload {
+ id: string;
name: string;
profileId: string;
projectId: string;
@@ -102,7 +107,10 @@ export async function getLiveVisitors(projectId: string) {
return keys.length;
}
-export async function getEvents(sql: string, options: GetEventsOptions = {}) {
+export async function getEvents(
+ sql: string,
+ options: GetEventsOptions = {}
+): Promise {
const events = await chQuery(sql);
if (options.profile) {
const profileIds = events.map((e) => e.profile_id);
@@ -124,7 +132,9 @@ export async function getEvents(sql: string, options: GetEventsOptions = {}) {
return events.map(transformEvent);
}
-export async function createEvent(payload: IServiceCreateEventPayload) {
+export async function createEvent(
+ payload: Omit
+) {
console.log(`create event ${payload.name} for ${payload.profileId}`);
if (payload.name === 'session_start') {
@@ -167,6 +177,7 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
}
const event: IClickhouseEvent = {
+ id: uuid(),
name: payload.name,
profile_id: payload.profileId,
project_id: payload.projectId,
@@ -193,6 +204,9 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
table: 'events',
values: [event],
format: 'JSONEachRow',
+ clickhouse_settings: {
+ date_time_input_format: 'best_effort',
+ },
});
redisPub.publish('event', JSON.stringify(transformEvent(event)));
@@ -208,3 +222,35 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
document: event,
};
}
+
+interface GetEventListOptions {
+ projectId: string;
+ profileId?: string;
+ take: number;
+ cursor?: string;
+}
+
+export async function getEventList({
+ cursor,
+ take,
+ projectId,
+ profileId,
+}: GetEventListOptions) {
+ const { sb, getSql } = createSqlBuilder();
+
+ sb.limit = take;
+ sb.where.projectId = `project_id = '${projectId}'`;
+ if (profileId) {
+ sb.where.profileId = `profile_id = '${profileId}'`;
+ }
+
+ if (cursor) {
+ sb.where.cursor = `created_at <= '${formatClickhouseDate(cursor)}'`;
+ }
+
+ sb.orderBy.created_at = 'created_at DESC';
+
+ const res = await getEvents(getSql(), { profile: true });
+
+ return res;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a53578a..60182cdc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -713,6 +713,9 @@ importers:
ramda:
specifier: ^0.29.1
version: 0.29.1
+ uuid:
+ specifier: ^9.0.1
+ version: 9.0.1
devDependencies:
'@mixan/eslint-config':
specifier: workspace:*
@@ -732,6 +735,9 @@ importers:
'@types/ramda':
specifier: ^0.29.6
version: 0.29.7
+ '@types/uuid':
+ specifier: ^9.0.8
+ version: 9.0.8
eslint:
specifier: ^8.48.0
version: 8.52.0
@@ -7645,6 +7651,10 @@ packages:
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
dev: false
+ /@types/uuid@9.0.8:
+ resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+ dev: true
+
/@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies: