Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions apps/api/src/query/builders/errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Analytics } from '../../types/tables';
import type { SimpleQueryConfig } from '../types';

export const ErrorsBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.errors>
> = {
export const ErrorsBuilders: Record<string, SimpleQueryConfig> = {
recent_errors: {
table: Analytics.errors,
fields: [
Expand Down
13 changes: 5 additions & 8 deletions apps/api/src/query/builders/pages.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Analytics } from '../../types/tables';
import type { SimpleQueryConfig } from '../types';

export const PagesBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.events>
> = {
export const PagesBuilders: Record<string, SimpleQueryConfig> = {
top_pages: {
table: Analytics.events,
fields: [
Expand Down Expand Up @@ -41,8 +38,8 @@ export const PagesBuilders: Record<
websiteId: string,
startDate: string,
endDate: string,
filters?: any[],
granularity?: any,
_filters?: unknown[],
_granularity?: unknown,
limit?: number,
offset?: number
) => ({
Expand Down Expand Up @@ -101,8 +98,8 @@ export const PagesBuilders: Record<
websiteId: string,
startDate: string,
endDate: string,
filters?: any[],
granularity?: any,
_filters?: unknown[],
_granularity?: unknown,
limit?: number,
offset?: number
) => ({
Expand Down
13 changes: 5 additions & 8 deletions apps/api/src/query/builders/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Analytics } from '../../types/tables';
import type { SimpleQueryConfig } from '../types';

export const ProfilesBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.events>
> = {
export const ProfilesBuilders: Record<string, SimpleQueryConfig> = {
profile_list: {
customSql: (
websiteId: string,
startDate: string,
endDate: string,
filters?: any[],
granularity?: any,
_filters?: unknown[],
_granularity?: unknown,
limit?: number,
offset?: number
) => ({
Expand Down Expand Up @@ -226,8 +223,8 @@ export const ProfilesBuilders: Record<
websiteId: string,
startDate: string,
endDate: string,
filters?: any[],
granularity?: any,
_filters?: unknown[],
_granularity?: unknown,
limit?: number,
offset?: number
) => ({
Expand Down
9 changes: 3 additions & 6 deletions apps/api/src/query/builders/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Analytics } from '../../types/tables';
import type { SimpleQueryConfig } from '../types';

export const SessionsBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.events>
> = {
export const SessionsBuilders: Record<string, SimpleQueryConfig> = {
session_metrics: {
table: Analytics.events,
fields: [
Expand Down Expand Up @@ -117,8 +114,8 @@ export const SessionsBuilders: Record<
websiteId: string,
startDate: string,
endDate: string,
filters?: any[],
granularity?: any,
_filters?: unknown[],
_granularity?: unknown,
limit?: number,
offset?: number
) => ({
Expand Down
11 changes: 4 additions & 7 deletions apps/api/src/query/builders/summary.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Analytics } from '../../types/tables';
import type { Filter, SimpleQueryConfig, TimeUnit } from '../types';

export const SummaryBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.events>
> = {
export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
summary_metrics: {
customSql: (
websiteId: string,
Expand Down Expand Up @@ -117,14 +114,14 @@ export const SummaryBuilders: Record<
websiteId: string,
startDate: string,
endDate: string,
_filters?: Filter[],
granularity?: TimeUnit,
_filters?: unknown[],
_granularity?: unknown,
_limit?: number,
_offset?: number,
timezone?: string
) => {
const tz = timezone || 'UTC';
const isHourly = granularity === 'hour' || granularity === 'hourly';
const isHourly = _granularity === 'hour' || _granularity === 'hourly';

if (isHourly) {
return {
Expand Down
5 changes: 1 addition & 4 deletions apps/api/src/query/builders/traffic.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Analytics } from '../../types/tables';
import type { SimpleQueryConfig } from '../types';

export const TrafficBuilders: Record<
string,
SimpleQueryConfig<typeof Analytics.events>
> = {
export const TrafficBuilders: Record<string, SimpleQueryConfig> = {
top_referrers: {
table: Analytics.events,
fields: [
Expand Down
103 changes: 65 additions & 38 deletions apps/api/src/query/screen-resolution-to-device-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Utility to map screen resolution to device type

export type DeviceType =
| 'mobile'
| 'tablet'
Expand All @@ -9,6 +7,69 @@ export type DeviceType =
| 'watch'
| 'unknown';

interface Resolution {
width: number;
height: number;
aspect: number;
}

function parseResolution(screenResolution: string): Resolution | null {
if (!screenResolution || typeof screenResolution !== 'string') {
return null;
}

const parts = screenResolution.split('x');
if (parts.length !== 2) {
return null;
}

const widthStr = parts[0];
const heightStr = parts[1];

if (typeof widthStr !== 'string' || typeof heightStr !== 'string') {
return null;
}

const width = Number.parseInt(widthStr, 10);
const height = Number.parseInt(heightStr, 10);

if (
Number.isNaN(width) ||
Number.isNaN(height) ||
width <= 0 ||
height <= 0
) {
return null;
}

return { width, height, aspect: width / height };
}

function determineDeviceType(resolution: Resolution): DeviceType {
const { width, aspect } = resolution;

if (width <= 400) {
return 'watch';
}
if (width <= 800 && aspect < 1.1) {
return 'mobile';
}
if (width > 800 && width <= 1280 && aspect >= 1.1 && aspect <= 1.7) {
return 'tablet';
}
if (width > 1280 && width <= 1920 && aspect >= 1.3 && aspect <= 1.8) {
return 'laptop';
}
if (aspect > 2.0 && width > 1920) {
return 'ultrawide';
}
if (width > 1920 && width <= 2560 && aspect >= 1.3 && aspect <= 2.0) {
return 'desktop';
}

return 'unknown';
}

/**
* Maps a screen resolution string (e.g., '375x812') to a device type.
* Uses width, height, and aspect ratio for more accurate mapping.
Expand All @@ -18,40 +79,6 @@ export type DeviceType =
export function mapScreenResolutionToDeviceType(
screenResolution: string
): DeviceType {
if (!screenResolution || typeof screenResolution !== 'string')
return 'unknown';
const parts = screenResolution.split('x');
if (
parts.length !== 2 ||
typeof parts[0] !== 'string' ||
typeof parts[1] !== 'string'
)
return 'unknown';
const width = Number.parseInt(parts[0], 10);
const height = Number.parseInt(parts[1], 10);
if (Number.isNaN(width) || Number.isNaN(height)) return 'unknown';
const aspect = width > 0 && height > 0 ? width / height : 0;

// Watches: very small screens
if (width <= 400 && height <= 400) return 'watch';

// Mobiles: small width, portrait aspect
if (width <= 800 && aspect < 1.1) return 'mobile';

// Tablets: medium width, aspect ratio between 1.1 and 1.7
if (width > 800 && width <= 1280 && aspect >= 1.1 && aspect <= 1.7)
return 'tablet';

// Laptops: width up to 1920, aspect ratio typical for laptops
if (width > 1280 && width <= 1920 && aspect >= 1.3 && aspect <= 1.8)
return 'laptop';

// Ultrawide: very wide aspect ratio
if (aspect > 2.0 && width > 1920) return 'ultrawide';

// Desktops: width up to 2560, aspect ratio typical for desktops
if (width > 1920 && width <= 2560 && aspect >= 1.3 && aspect <= 2.0)
return 'desktop';

return 'unknown';
const resolution = parseResolution(screenResolution);
return resolution ? determineDeviceType(resolution) : 'unknown';
}
20 changes: 15 additions & 5 deletions apps/api/src/query/simple-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ import { FilterOperators } from './types';
import { applyPlugins } from './utils';

export class SimpleQueryBuilder {
private config: SimpleQueryConfig;
private request: QueryRequest;
private websiteDomain?: string | null;

constructor(
private config: SimpleQueryConfig,
private request: QueryRequest,
private websiteDomain?: string | null
) {}
config: SimpleQueryConfig,
request: QueryRequest,
websiteDomain?: string | null
) {
this.config = config;
this.request = request;
this.websiteDomain = websiteDomain;
}

private buildFilter(
filter: Filter,
Expand Down Expand Up @@ -138,7 +146,9 @@ export class SimpleQueryBuilder {
if (this.request.filters) {
for (let i = 0; i < this.request.filters.length; i++) {
const filter = this.request.filters[i];
if (!filter) continue;
if (!filter) {
continue;
}
const { clause, params: filterParams } = this.buildFilter(filter, i);
whereClause.push(clause);
Object.assign(params, filterParams);
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/query/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { z } from 'zod';

export const FilterOperators = {
eq: '=',
ne: '!=',
Expand Down Expand Up @@ -40,7 +38,7 @@ export interface SimpleQueryConfig {
plugins?: {
parseReferrers?: boolean;
normalizeUrls?: boolean;
[key: string]: any;
[key: string]: unknown;
};
customSql?: (
websiteId: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ interface Props {
funnelId: string;
dateRange: { start_date: string; end_date: string };
onReferrerChange?: (referrer: string) => void;
data: UseTRPCQueryResult<any, any>['data'];
data: UseTRPCQueryResult<
{ referrer_analytics: FunnelAnalyticsByReferrerResult[] },
TRPCClientErrorLike<AppRouter>
>['data'];
isLoading: boolean;
error: TRPCClientErrorLike<AppRouter> | null;
}

export default function FunnelAnalyticsByReferrer({
websiteId,
funnelId,
dateRange,
onReferrerChange,
data,
isLoading,
Expand All @@ -46,10 +46,16 @@ export default function FunnelAnalyticsByReferrer({

// Group referrers strictly by domain (lowercased, fallback to 'direct')
const referrers = useMemo(() => {
if (!data?.referrer_analytics) return [];
if (!data?.referrer_analytics) {
return [];
}
const grouped = new Map<
string,
{ label: string; parsed: any; users: number }
{
label: string;
parsed: FunnelAnalyticsByReferrerResult['referrer_parsed'];
users: number;
}
>();
for (const r of data.referrer_analytics) {
const domain = r.referrer_parsed?.domain?.toLowerCase() || 'direct';
Expand Down Expand Up @@ -154,7 +160,7 @@ export default function FunnelAnalyticsByReferrer({
</Badge>
</div>
</SelectItem>
{referrers.map((option: any) => (
{referrers.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex w-full items-center gap-2">
<ReferrerSourceCell
Expand Down
Loading
Loading