feature(dashboard,api): add timezone support

* feat(dashboard): add support for today, yesterday etc (timezones)

* fix(db): escape js dates

* fix(dashboard): ensure we support default timezone

* final fixes

* remove complete series and add sql with fill instead
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-05-23 11:26:44 +02:00
committed by GitHub
parent 46bfeee131
commit 680727355b
48 changed files with 1817 additions and 758 deletions

View File

@@ -1,4 +1,4 @@
import type { ResponseJSON } from '@clickhouse/client';
import type { ClickHouseSettings, ResponseJSON } from '@clickhouse/client';
import { ClickHouseLogLevel, createClient } from '@clickhouse/client';
import { escape } from 'sqlstring';
@@ -11,7 +11,6 @@ export { createClient };
const logger = createLogger({ name: 'clickhouse' });
import type { Logger } from '@clickhouse/client';
import { getTimezoneFromDateString } from '@openpanel/common';
// All three LogParams types are exported by the client
interface LogParams {
@@ -142,10 +141,12 @@ export const ch = new Proxy(originalCh, {
export async function chQueryWithMeta<T extends Record<string, any>>(
query: string,
clickhouseSettings?: ClickHouseSettings,
): Promise<ResponseJSON<T>> {
const start = Date.now();
const res = await ch.query({
query,
clickhouse_settings: clickhouseSettings,
});
const json = await res.json<T>();
const keys = Object.keys(json.data[0] || {});
@@ -170,6 +171,7 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
rows: json.rows,
stats: response.statistics,
elapsed: Date.now() - start,
clickhouseSettings,
});
return response;
@@ -177,8 +179,9 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
export async function chQuery<T extends Record<string, any>>(
query: string,
clickhouseSettings?: ClickHouseSettings,
): Promise<T[]> {
return (await chQueryWithMeta<T>(query)).data;
return (await chQueryWithMeta<T>(query, clickhouseSettings)).data;
}
export function formatClickhouseDate(
@@ -188,7 +191,10 @@ export function formatClickhouseDate(
if (skipTime) {
return new Date(date).toISOString().split('T')[0]!;
}
return new Date(date).toISOString().replace('T', ' ').replace(/Z+$/, '');
return new Date(date)
.toISOString()
.replace('T', ' ')
.replace(/(\.\d{3})?Z+$/, '');
}
export function toDate(str: string, interval?: IInterval) {

View File

@@ -73,7 +73,11 @@ export class Query<T = any> {
};
private _transform?: Record<string, (item: T) => any>;
private _union?: Query;
constructor(private client: ClickHouseClient) {}
private _dateRegex = /\d{4}-\d{2}-\d{2}([\s\:\d\.]+)?/g;
constructor(
private client: ClickHouseClient,
private timezone: string,
) {}
// Select methods
select<U>(
@@ -121,9 +125,14 @@ export class Query<T = any> {
if (Array.isArray(value)) {
return `(${value.map((v) => this.escapeValue(v)).join(', ')})`;
}
if (value instanceof Date) {
return escape(clix.datetime(value));
if (
(typeof value === 'string' && this._dateRegex.test(value)) ||
value instanceof Date
) {
return this.escapeDate(value);
}
return escape(value);
}
@@ -249,10 +258,10 @@ export class Query<T = any> {
private escapeDate(value: string | Date): string {
if (value instanceof Date) {
return clix.datetime(value);
return escape(clix.datetime(value));
}
return value.replaceAll(/\d{4}-\d{2}-\d{2}([\s\:\d\.]+)?/g, (match) => {
return value.replaceAll(this._dateRegex, (match) => {
return escape(match);
});
}
@@ -348,7 +357,10 @@ export class Query<T = any> {
// SELECT
if (this._select.length > 0) {
parts.push('SELECT', this._select.map(this.escapeDate).join(', '));
parts.push(
'SELECT',
this._select.map((col) => this.escapeDate(col)).join(', '),
);
} else {
parts.push('SELECT *');
}
@@ -483,26 +495,16 @@ export class Query<T = any> {
// Execution methods
async execute(): Promise<T[]> {
const query = this.buildQuery();
console.log('TEST QUERY ----->');
console.log(query);
console.log('<----------');
const perf = performance.now();
try {
const result = await this.client.query({
query,
});
const json = await result.json<T>();
const perf2 = performance.now();
console.log(`PERF: ${perf2 - perf}ms`);
return this.transformJson(json).data;
} catch (error) {
console.log('ERROR ----->');
console.log(error);
console.log('<----------');
console.log(query);
console.log('<----------');
throw error;
}
console.log('query', query);
const result = await this.client.query({
query,
clickhouse_settings: {
session_timezone: this.timezone,
},
});
const json = await result.json<T>();
return this.transformJson(json).data;
}
// Debug methods
@@ -535,7 +537,7 @@ export class Query<T = any> {
}
clone(): Query<T> {
return new Query(this.client).merge(this);
return new Query(this.client, this.timezone).merge(this);
}
// Add merge method
@@ -629,12 +631,8 @@ export class WhereGroupBuilder {
}
// Helper function to create a new query
export function createQuery(client: ClickHouseClient): Query {
return new Query(client);
}
export function clix(client: ClickHouseClient): Query {
return new Query(client);
export function clix(client: ClickHouseClient, timezone?: string): Query {
return new Query(client, timezone ?? 'UTC');
}
clix.exp = (expr: string | Query<any>) =>
@@ -654,7 +652,7 @@ clix.dynamicDatetime = (date: string | Date, interval: IInterval) => {
return clix.datetime(date);
};
clix.toStartOf = (node: string, interval: IInterval) => {
clix.toStartOf = (node: string, interval: IInterval, timezone?: string) => {
switch (interval) {
case 'minute': {
return `toStartOfMinute(${node})`;
@@ -666,10 +664,12 @@ clix.toStartOf = (node: string, interval: IInterval) => {
return `toStartOfDay(${node})`;
}
case 'week': {
return `toStartOfWeek(${node})`;
// Does not respect timezone settings (session_timezone) so we need to pass it manually
return `toStartOfWeek(${node}${timezone ? `, 1, '${timezone}'` : ''})`;
}
case 'month': {
return `toStartOfMonth(${node})`;
// Does not respect timezone settings (session_timezone) so we need to pass it manually
return `toStartOfMonth(${node}${timezone ? `, '${timezone}'` : ''})`;
}
}
};

View File

@@ -1,19 +1,12 @@
import { escape } from 'sqlstring';
import {
getTimezoneFromDateString,
stripLeadingAndTrailingSlashes,
} from '@openpanel/common';
import { stripLeadingAndTrailingSlashes } from '@openpanel/common';
import type {
IChartEventFilter,
IGetChartDataInput,
} from '@openpanel/validation';
import {
TABLE_NAMES,
formatClickhouseDate,
toDate,
} from '../clickhouse/client';
import { TABLE_NAMES, formatClickhouseDate } from '../clickhouse/client';
import { createSqlBuilder } from '../sql-builder';
export function transformPropertyKey(property: string) {
@@ -61,9 +54,9 @@ export function getChartSql({
startDate,
endDate,
projectId,
chartType,
limit,
}: IGetChartDataInput) {
timezone,
}: IGetChartDataInput & { timezone: string }) {
const {
sb,
join,
@@ -73,6 +66,7 @@ export function getChartSql({
getSelect,
getOrderBy,
getGroupBy,
getFill,
} = createSqlBuilder();
sb.where = getEventFiltersWhereClause(event.filters);
@@ -99,34 +93,40 @@ export function getChartSql({
sb.select.count = 'count(*) as count';
switch (interval) {
case 'minute': {
sb.select.date = `toStartOfMinute(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
sb.fill = `FROM toStartOfMinute(toDateTime('${startDate}')) TO toStartOfMinute(toDateTime('${endDate}')) STEP toIntervalMinute(1)`;
sb.select.date = 'toStartOfMinute(created_at) as date';
break;
}
case 'hour': {
sb.select.date = `toStartOfHour(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
sb.fill = `FROM toStartOfHour(toDateTime('${startDate}')) TO toStartOfHour(toDateTime('${endDate}')) STEP toIntervalHour(1)`;
sb.select.date = 'toStartOfHour(created_at) as date';
break;
}
case 'day': {
sb.select.date = `toStartOfDay(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
sb.fill = `FROM toStartOfDay(toDateTime('${startDate}')) TO toStartOfDay(toDateTime('${endDate}')) STEP toIntervalDay(1)`;
sb.select.date = 'toStartOfDay(created_at) as date';
break;
}
case 'week': {
sb.select.date = `toStartOfWeek(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
sb.fill = `FROM toStartOfWeek(toDateTime('${startDate}'), 1, '${timezone}') TO toStartOfWeek(toDateTime('${endDate}'), 1, '${timezone}') STEP toIntervalWeek(1)`;
sb.select.date = `toStartOfWeek(created_at, 1, '${timezone}') as date`;
break;
}
case 'month': {
sb.select.date = `toStartOfMonth(toTimeZone(created_at, '${getTimezoneFromDateString(startDate)}')) as date`;
sb.fill = `FROM toStartOfMonth(toDateTime('${startDate}'), '${timezone}') TO toStartOfMonth(toDateTime('${endDate}'), '${timezone}') STEP toIntervalMonth(1)`;
sb.select.date = `toStartOfMonth(created_at, '${timezone}') as date`;
break;
}
}
sb.groupBy.date = 'date';
sb.orderBy.date = 'date ASC';
if (startDate) {
sb.where.startDate = `${toDate('created_at', interval)} >= ${toDate(formatClickhouseDate(startDate), interval)}`;
sb.where.startDate = `created_at >= toDateTime('${formatClickhouseDate(startDate)}')`;
}
if (endDate) {
sb.where.endDate = `${toDate('created_at', interval)} <= ${toDate(formatClickhouseDate(endDate), interval)}`;
sb.where.endDate = `created_at <= toDateTime('${formatClickhouseDate(endDate)}')`;
}
if (breakdowns.length > 0 && limit) {
@@ -179,18 +179,14 @@ export function getChartSql({
ORDER BY profile_id, created_at DESC
) as subQuery`;
console.log(
`${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()}`,
);
return `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()}`;
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
console.log('CHART SQL', sql);
return sql;
}
console.log(
`${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()}`,
);
return `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()}`;
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
console.log('CHART SQL', sql);
return sql;
}
export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {

View File

@@ -1,5 +1,5 @@
import type { ClickHouseClient } from '@clickhouse/client';
import { Query, createQuery } from '../clickhouse/query-builder';
import { clix } from '../clickhouse/query-builder';
export interface Insight {
type: string;
@@ -73,7 +73,7 @@ export class InsightsService {
constructor(private client: ClickHouseClient) {}
private async getTrafficSpikes(projectId: string): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'referrer_name',
'toDate(created_at) as date',
@@ -100,7 +100,7 @@ export class InsightsService {
}
private async getEventSurges(projectId: string): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'toDate(created_at) as date',
'COUNT(*) as event_count',
@@ -126,7 +126,7 @@ export class InsightsService {
}
private async getNewVisitorTrends(projectId: string): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'toMonth(created_at) as month',
'COUNT(DISTINCT device_id) as new_visitors',
@@ -155,7 +155,7 @@ export class InsightsService {
private async getReferralSourceHighlights(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'referrer_name',
'COUNT(*) as count',
@@ -179,7 +179,7 @@ export class InsightsService {
private async getSessionDurationChanges(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'toWeek(created_at) as week',
'avg(duration) as avg_duration',
@@ -205,7 +205,7 @@ export class InsightsService {
}
private async getTopPerformingContent(projectId: string): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'path',
'COUNT(*) as view_count',
@@ -233,7 +233,7 @@ export class InsightsService {
private async getBounceRateImprovements(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'toMonth(created_at) as month',
'sum(is_bounce) / COUNT(*) as bounce_rate',
@@ -261,7 +261,7 @@ export class InsightsService {
private async getReturningVisitorTrends(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'toQuarter(created_at) as quarter',
'COUNT(DISTINCT device_id) as returning_visitors',
@@ -290,7 +290,7 @@ export class InsightsService {
private async getGeographicInterestShifts(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'country',
'COUNT(*) as visitor_count',
@@ -318,7 +318,7 @@ export class InsightsService {
private async getEventCompletionChanges(
projectId: string,
): Promise<Insight[]> {
const query = createQuery(this.client)
const query = clix(this.client)
.select([
'event_name',
'toMonth(created_at) as month',

View File

@@ -14,10 +14,6 @@ export type IServiceMember = Prisma.MemberGetPayload<{
}> & { access: ProjectAccess[] };
export type IServiceProjectAccess = ProjectAccess;
export function transformOrganization<T>(org: T) {
return org;
}
export async function getOrganizations(userId: string | null) {
if (!userId) return [];
@@ -34,10 +30,10 @@ export async function getOrganizations(userId: string | null) {
},
});
return organizations.map(transformOrganization);
return organizations;
}
export function getOrganizationBySlug(slug: string) {
export function getOrganizationById(slug: string) {
return db.organization.findUniqueOrThrow({
where: {
id: slug,
@@ -59,7 +55,7 @@ export async function getOrganizationByProjectId(projectId: string) {
return null;
}
return transformOrganization(project.organization);
return project.organization;
}
export const getOrganizationByProjectIdCached = cacheable(
@@ -258,3 +254,32 @@ export async function getOrganizationSubscriptionChartEndDate(
return endDate;
}
const DEFAULT_TIMEZONE = 'UTC';
export async function getSettingsForOrganization(organizationId: string) {
const organization = await db.organization.findUniqueOrThrow({
where: {
id: organizationId,
},
});
return {
timezone: organization.timezone || DEFAULT_TIMEZONE,
};
}
export async function getSettingsForProject(projectId: string) {
const project = await db.project.findUniqueOrThrow({
where: {
id: projectId,
},
include: {
organization: true,
},
});
return {
timezone: project.organization.timezone || DEFAULT_TIMEZONE,
};
}

View File

@@ -15,7 +15,9 @@ export const zGetMetricsInput = z.object({
interval: zTimeInterval,
});
export type IGetMetricsInput = z.infer<typeof zGetMetricsInput>;
export type IGetMetricsInput = z.infer<typeof zGetMetricsInput> & {
timezone: string;
};
export const zGetTopPagesInput = z.object({
projectId: z.string(),
@@ -27,7 +29,9 @@ export const zGetTopPagesInput = z.object({
limit: z.number().optional(),
});
export type IGetTopPagesInput = z.infer<typeof zGetTopPagesInput>;
export type IGetTopPagesInput = z.infer<typeof zGetTopPagesInput> & {
timezone: string;
};
export const zGetTopEntryExitInput = z.object({
projectId: z.string(),
@@ -40,7 +44,9 @@ export const zGetTopEntryExitInput = z.object({
limit: z.number().optional(),
});
export type IGetTopEntryExitInput = z.infer<typeof zGetTopEntryExitInput>;
export type IGetTopEntryExitInput = z.infer<typeof zGetTopEntryExitInput> & {
timezone: string;
};
export const zGetTopGenericInput = z.object({
projectId: z.string(),
@@ -75,7 +81,9 @@ export const zGetTopGenericInput = z.object({
limit: z.number().optional(),
});
export type IGetTopGenericInput = z.infer<typeof zGetTopGenericInput>;
export type IGetTopGenericInput = z.infer<typeof zGetTopGenericInput> & {
timezone: string;
};
export class OverviewService {
private pendingQueries: Map<string, Promise<number | null>> = new Map();
@@ -91,11 +99,13 @@ export class OverviewService {
startDate,
endDate,
filters,
timezone,
}: {
projectId: string;
startDate: string;
endDate: string;
filters: IChartEventFilter[];
timezone: string;
}) {
const where = this.getRawWhereClause('sessions', filters);
const key = `total_sessions_${projectId}_${startDate}_${endDate}_${JSON.stringify(filters)}`;
@@ -109,15 +119,15 @@ export class OverviewService {
// Create new query promise and store it
const queryPromise = getCache(key, 15, async () => {
try {
const result = await clix(this.client)
const result = await clix(this.client, timezone)
.select<{
total_sessions: number;
}>(['sum(sign) as total_sessions'])
.from(TABLE_NAMES.sessions, true)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(where)
.having('sum(sign)', '>', 0)
@@ -138,6 +148,7 @@ export class OverviewService {
startDate,
endDate,
interval,
timezone,
}: IGetMetricsInput): Promise<{
metrics: {
bounce_rate: number;
@@ -160,17 +171,17 @@ export class OverviewService {
const where = this.getRawWhereClause('sessions', filters);
if (this.isPageFilter(filters)) {
// Session aggregation with bounce rates
const sessionAggQuery = clix(this.client)
const sessionAggQuery = clix(this.client, timezone)
.select([
`${clix.toStartOfInterval('created_at', interval, startDate)} AS date`,
`${clix.toStartOf('created_at', interval, timezone)} AS date`,
'round((countIf(is_bounce = 1 AND sign = 1) * 100.) / countIf(sign = 1), 2) AS bounce_rate',
])
.from(TABLE_NAMES.sessions, true)
.where('sign', '=', 1)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(where)
.groupBy(['date'])
@@ -178,7 +189,7 @@ export class OverviewService {
.orderBy('date', 'ASC');
// Overall unique visitors
const overallUniqueVisitorsQuery = clix(this.client)
const overallUniqueVisitorsQuery = clix(this.client, timezone)
.select([
'uniq(profile_id) AS unique_visitors',
'uniq(session_id) AS total_sessions',
@@ -187,23 +198,23 @@ export class OverviewService {
.where('project_id', '=', projectId)
.where('name', '=', 'screen_view')
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(this.getRawWhereClause('events', filters));
return clix(this.client)
return clix(this.client, timezone)
.with('session_agg', sessionAggQuery)
.with(
'overall_bounce_rate',
clix(this.client)
clix(this.client, timezone)
.select(['bounce_rate'])
.from('session_agg')
.where('date', '=', clix.exp("'1970-01-01 00:00:00'")),
)
.with(
'daily_stats',
clix(this.client)
clix(this.client, timezone)
.select(['date', 'bounce_rate'])
.from('session_agg')
.where('date', '!=', clix.exp("'1970-01-01 00:00:00'")),
@@ -221,7 +232,7 @@ export class OverviewService {
overall_total_sessions: number;
overall_bounce_rate: number;
}>([
`${clix.toStartOfInterval('e.created_at', interval, startDate)} AS date`,
`${clix.toInterval('e.created_at', interval)} AS date`,
'ds.bounce_rate as bounce_rate',
'uniq(e.profile_id) AS unique_visitors',
'uniq(e.session_id) AS total_sessions',
@@ -236,20 +247,29 @@ export class OverviewService {
.from(`${TABLE_NAMES.events} AS e`)
.leftJoin(
'daily_stats AS ds',
`${clix.toStartOfInterval('e.created_at', interval, startDate)} = ds.date`,
`${clix.toInterval('e.created_at', interval)} = ds.date`,
)
.where('e.project_id', '=', projectId)
.where('e.name', '=', 'screen_view')
.where('e.created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(this.getRawWhereClause('events', filters))
.groupBy(['date', 'ds.bounce_rate'])
.orderBy('date', 'ASC')
.fill(
clix.toStartOfInterval(clix.datetime(startDate), interval, startDate),
clix.toStartOfInterval(clix.datetime(endDate), interval, startDate),
clix.toStartOf(
clix.datetime(
startDate,
['month', 'week'].includes(interval) ? 'toDate' : 'toDateTime',
),
interval,
),
clix.datetime(
endDate,
['month', 'week'].includes(interval) ? 'toDate' : 'toDateTime',
),
clix.toInterval('1', interval),
)
.transform({
@@ -289,7 +309,7 @@ export class OverviewService {
});
}
const query = clix(this.client)
const query = clix(this.client, timezone)
.select<{
date: string;
bounce_rate: number;
@@ -299,7 +319,7 @@ export class OverviewService {
total_screen_views: number;
views_per_session: number;
}>([
`${clix.toStartOfInterval('created_at', interval, startDate)} AS date`,
`${clix.toStartOf('created_at', interval, timezone)} AS date`,
'round(sum(sign * is_bounce) * 100.0 / sum(sign), 2) as bounce_rate',
'uniqIf(profile_id, sign > 0) AS unique_visitors',
'sum(sign) AS total_sessions',
@@ -310,8 +330,8 @@ export class OverviewService {
])
.from('sessions')
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.where('project_id', '=', projectId)
.rawWhere(where)
@@ -320,8 +340,17 @@ export class OverviewService {
.rollup()
.orderBy('date', 'ASC')
.fill(
clix.toStartOfInterval(clix.datetime(startDate), interval, startDate),
clix.toStartOfInterval(clix.datetime(endDate), interval, startDate),
clix.toStartOf(
clix.datetime(
startDate,
['month', 'week'].includes(interval) ? 'toDate' : 'toDateTime',
),
interval,
),
clix.datetime(
endDate,
['month', 'week'].includes(interval) ? 'toDate' : 'toDateTime',
),
clix.toInterval('1', interval),
)
.transform({
@@ -384,8 +413,9 @@ export class OverviewService {
endDate,
cursor = 1,
limit = 10,
timezone,
}: IGetTopPagesInput) {
const pageStatsQuery = clix(this.client)
const pageStatsQuery = clix(this.client, timezone)
.select([
'origin',
'path',
@@ -398,15 +428,15 @@ export class OverviewService {
.where('name', '=', 'screen_view')
.where('path', '!=', '')
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.groupBy(['origin', 'path'])
.orderBy('count', 'DESC')
.limit(limit)
.offset((cursor - 1) * limit);
const bounceStatsQuery = clix(this.client)
const bounceStatsQuery = clix(this.client, timezone)
.select([
'entry_path',
'entry_origin',
@@ -416,15 +446,15 @@ export class OverviewService {
.where('sign', '=', 1)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.groupBy(['entry_path', 'entry_origin']);
pageStatsQuery.rawWhere(this.getRawWhereClause('events', filters));
bounceStatsQuery.rawWhere(this.getRawWhereClause('sessions', filters));
const mainQuery = clix(this.client)
const mainQuery = clix(this.client, timezone)
.with('page_stats', pageStatsQuery)
.with('bounce_stats', bounceStatsQuery)
.select<{
@@ -455,6 +485,7 @@ export class OverviewService {
startDate,
endDate,
filters,
timezone,
});
return mainQuery.execute();
@@ -468,6 +499,7 @@ export class OverviewService {
mode,
cursor = 1,
limit = 10,
timezone,
}: IGetTopEntryExitInput) {
const where = this.getRawWhereClause('sessions', filters);
@@ -476,11 +508,12 @@ export class OverviewService {
filters,
startDate,
endDate,
timezone,
});
const offset = (cursor - 1) * limit;
const query = clix(this.client)
const query = clix(this.client, timezone)
.select<{
origin: string;
path: string;
@@ -497,8 +530,8 @@ export class OverviewService {
.from(TABLE_NAMES.sessions, true)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(where)
.groupBy([`${mode}_origin`, `${mode}_path`])
@@ -510,7 +543,7 @@ export class OverviewService {
let mainQuery = query;
if (this.isPageFilter(filters)) {
mainQuery = clix(this.client)
mainQuery = clix(this.client, timezone)
.with('distinct_sessions', distinctSessionQuery)
.merge(query)
.where(
@@ -525,6 +558,7 @@ export class OverviewService {
startDate,
endDate,
filters,
timezone,
});
return mainQuery.execute();
@@ -535,19 +569,21 @@ export class OverviewService {
filters,
startDate,
endDate,
timezone,
}: {
projectId: string;
filters: IChartEventFilter[];
startDate: string;
endDate: string;
timezone: string;
}) {
return clix(this.client)
return clix(this.client, timezone)
.select(['DISTINCT session_id'])
.from(TABLE_NAMES.events)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.rawWhere(this.getRawWhereClause('events', filters));
}
@@ -560,12 +596,14 @@ export class OverviewService {
column,
cursor = 1,
limit = 10,
timezone,
}: IGetTopGenericInput) {
const distinctSessionQuery = this.getDistinctSessions({
projectId,
filters,
startDate,
endDate,
timezone,
});
const prefixColumn = (() => {
@@ -584,7 +622,7 @@ export class OverviewService {
const offset = (cursor - 1) * limit;
const query = clix(this.client)
const query = clix(this.client, timezone)
.select<{
prefix?: string;
name: string;
@@ -601,8 +639,8 @@ export class OverviewService {
.from(TABLE_NAMES.sessions, true)
.where('project_id', '=', projectId)
.where('created_at', 'BETWEEN', [
clix.datetime(startDate),
clix.datetime(endDate),
clix.datetime(startDate, 'toDateTime'),
clix.datetime(endDate, 'toDateTime'),
])
.groupBy([prefixColumn, column].filter(Boolean))
.having('sum(sign)', '>', 0)
@@ -613,7 +651,7 @@ export class OverviewService {
let mainQuery = query;
if (this.isPageFilter(filters)) {
mainQuery = clix(this.client)
mainQuery = clix(this.client, timezone)
.with('distinct_sessions', distinctSessionQuery)
.merge(query)
.where(
@@ -632,6 +670,7 @@ export class OverviewService {
startDate,
endDate,
filters,
timezone,
}),
]);

View File

@@ -10,6 +10,7 @@ export interface SqlBuilderObject {
joins: Record<string, string>;
limit: number | undefined;
offset: number | undefined;
fill: string | undefined;
}
export function createSqlBuilder() {
@@ -26,6 +27,7 @@ export function createSqlBuilder() {
joins: {},
limit: undefined,
offset: undefined,
fill: undefined,
};
const getWhere = () =>
@@ -43,6 +45,7 @@ export function createSqlBuilder() {
const getOffset = () => (sb.offset ? `OFFSET ${sb.offset}` : '');
const getJoins = () =>
Object.keys(sb.joins).length ? join(sb.joins, ' ') : '';
const getFill = () => (sb.fill ? `WITH FILL ${sb.fill}` : '');
return {
sb,
@@ -54,6 +57,7 @@ export function createSqlBuilder() {
getOrderBy,
getHaving,
getJoins,
getFill,
getSql: () => {
const sql = [
getSelect(),
@@ -65,6 +69,7 @@ export function createSqlBuilder() {
getOrderBy(),
getLimit(),
getOffset(),
getFill(),
]
.filter(Boolean)
.join(' ');