Skip to content

Commit 437d7d2

Browse files
committed
query api telemetry
1 parent ad7d109 commit 437d7d2

File tree

5 files changed

+209
-167
lines changed

5 files changed

+209
-167
lines changed

apps/api/src/routes/query.ts

Lines changed: 183 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { auth } from "@databuddy/auth";
22
import { and, apikeyAccess, db, eq, isNull, websites } from "@databuddy/db";
33
import { filterOptions } from "@databuddy/shared/lists/filters";
4-
import { record, setAttributes } from "../lib/tracing";
54
import { Elysia, t } from "elysia";
65
import { getApiKeyFromHeader, isApiKeyPresent } from "../lib/api-key";
6+
import { record, setAttributes } from "../lib/tracing";
77
import { getCachedWebsiteDomain, getWebsiteDomain } from "../lib/website-utils";
88
import { compileQuery, executeQuery } from "../query";
99
import { QueryBuilders } from "../query/builders";
@@ -335,185 +335,208 @@ export const query = new Elysia({ prefix: "/v1/query" })
335335
}
336336
);
337337

338-
async function executeDynamicQuery(
338+
function executeDynamicQuery(
339339
request: DynamicQueryRequestType,
340340
queryParams: QueryParams,
341341
domainCache?: Record<string, string | null>
342342
) {
343-
const startDate = queryParams.start_date || queryParams.startDate;
344-
const endDate = queryParams.end_date || queryParams.endDate;
345-
const websiteId = queryParams.website_id;
346-
347-
const websiteDomain = websiteId
348-
? (domainCache?.[websiteId] ?? (await getWebsiteDomain(websiteId)))
349-
: null;
343+
return record("query.execute_dynamic", async () => {
344+
const startDate = queryParams.start_date || queryParams.startDate;
345+
const endDate = queryParams.end_date || queryParams.endDate;
346+
const websiteId = queryParams.website_id;
347+
348+
setAttributes({
349+
"query.id": request.id || "anonymous",
350+
"query.website_id": websiteId || "unknown",
351+
"query.parameters": JSON.stringify(
352+
request.parameters.map((p) => (typeof p === "string" ? p : p.name))
353+
),
354+
"query.granularity": request.granularity || "day",
355+
"query.time_range.start": startDate || "unknown",
356+
"query.time_range.end": endDate || "unknown",
357+
"query.filters.count": request.filters?.length || 0,
358+
});
350359

351-
const MAX_HOURLY_DAYS = 7;
352-
const MS_PER_DAY = 1000 * 60 * 60 * 24;
360+
const websiteDomain = websiteId
361+
? (domainCache?.[websiteId] ?? (await getWebsiteDomain(websiteId)))
362+
: null;
353363

354-
const validateHourlyDateRange = (start: string, end: string) => {
355-
const rangeDays = Math.ceil(
356-
(new Date(end).getTime() - new Date(start).getTime()) / MS_PER_DAY
357-
);
364+
const MAX_HOURLY_DAYS = 7;
365+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
358366

359-
if (rangeDays > MAX_HOURLY_DAYS) {
360-
throw new Error(
361-
`Hourly granularity only supports ranges up to ${MAX_HOURLY_DAYS} days. Use daily granularity for longer periods.`
367+
const validateHourlyDateRange = (start: string, end: string) => {
368+
const rangeDays = Math.ceil(
369+
(new Date(end).getTime() - new Date(start).getTime()) / MS_PER_DAY
362370
);
363-
}
364-
};
365371

366-
const getTimeUnit = (
367-
granularity?: string,
368-
startDate?: string,
369-
endDate?: string
370-
): "hour" | "day" => {
371-
const isHourly = ["hourly", "hour"].includes(granularity || "");
372-
373-
if (isHourly) {
374-
if (startDate && endDate) {
375-
validateHourlyDateRange(startDate, endDate);
372+
if (rangeDays > MAX_HOURLY_DAYS) {
373+
throw new Error(
374+
`Hourly granularity only supports ranges up to ${MAX_HOURLY_DAYS} days. Use daily granularity for longer periods.`
375+
);
376+
}
377+
};
378+
379+
const getTimeUnit = (
380+
granularity?: string,
381+
startDate?: string,
382+
endDate?: string
383+
): "hour" | "day" => {
384+
const isHourly = ["hourly", "hour"].includes(granularity || "");
385+
386+
if (isHourly) {
387+
if (startDate && endDate) {
388+
validateHourlyDateRange(startDate, endDate);
389+
}
390+
return "hour";
376391
}
377-
return "hour";
378-
}
379392

380-
return "day";
381-
};
393+
return "day";
394+
};
395+
396+
function validateParameterRequest(
397+
parameter: string,
398+
siteId: string | undefined,
399+
start: string | undefined,
400+
end: string | undefined
401+
):
402+
| { success: true; siteId: string; start: string; end: string }
403+
| { success: false; error: string } {
404+
if (!QueryBuilders[parameter]) {
405+
return {
406+
success: false,
407+
error: `Unknown query type: ${parameter}`,
408+
};
409+
}
382410

383-
function validateParameterRequest(
384-
parameter: string,
385-
siteId: string | undefined,
386-
start: string | undefined,
387-
end: string | undefined
388-
):
389-
| { success: true; siteId: string; start: string; end: string }
390-
| { success: false; error: string } {
391-
if (!QueryBuilders[parameter]) {
392-
return {
393-
success: false,
394-
error: `Unknown query type: ${parameter}`,
395-
};
396-
}
411+
if (!(siteId && start && end)) {
412+
return {
413+
success: false,
414+
error:
415+
"Missing required parameters: website_id, start_date, or end_date",
416+
};
417+
}
397418

398-
if (!(siteId && start && end)) {
399-
return {
400-
success: false,
401-
error:
402-
"Missing required parameters: website_id, start_date, or end_date",
403-
};
419+
return { success: true, siteId, start, end };
404420
}
405421

406-
return { success: true, siteId, start, end };
407-
}
408-
409-
async function processParameter(
410-
parameterInput:
411-
| string
412-
| {
413-
name: string;
414-
start_date?: string;
415-
end_date?: string;
416-
granularity?: string;
417-
id?: string;
418-
},
419-
dynamicRequest: DynamicQueryRequestType,
420-
params: QueryParams,
421-
siteId: string | undefined,
422-
defaultStart: string | undefined,
423-
defaultEnd: string | undefined,
424-
domain: string | null
425-
) {
426-
const isObject = typeof parameterInput === "object";
427-
const parameterName = isObject ? parameterInput.name : parameterInput;
428-
const customId =
429-
isObject && parameterInput.id ? parameterInput.id : parameterName;
430-
const paramStart =
431-
isObject && parameterInput.start_date
432-
? parameterInput.start_date
433-
: defaultStart;
434-
const paramEnd =
435-
isObject && parameterInput.end_date
436-
? parameterInput.end_date
437-
: defaultEnd;
438-
const paramGranularity =
439-
isObject && parameterInput.granularity
440-
? parameterInput.granularity
441-
: dynamicRequest.granularity;
442-
443-
const validation = validateParameterRequest(
444-
parameterName,
445-
siteId,
446-
paramStart,
447-
paramEnd
448-
);
449-
if (!validation.success) {
450-
return {
451-
parameter: customId,
452-
success: false,
453-
error: validation.error,
454-
data: [],
455-
};
456-
}
422+
async function processParameter(
423+
parameterInput:
424+
| string
425+
| {
426+
name: string;
427+
start_date?: string;
428+
end_date?: string;
429+
granularity?: string;
430+
id?: string;
431+
},
432+
dynamicRequest: DynamicQueryRequestType,
433+
params: QueryParams,
434+
siteId: string | undefined,
435+
defaultStart: string | undefined,
436+
defaultEnd: string | undefined,
437+
domain: string | null
438+
) {
439+
const isObject = typeof parameterInput === "object";
440+
const parameterName = isObject ? parameterInput.name : parameterInput;
441+
const customId =
442+
isObject && parameterInput.id ? parameterInput.id : parameterName;
443+
const paramStart =
444+
isObject && parameterInput.start_date
445+
? parameterInput.start_date
446+
: defaultStart;
447+
const paramEnd =
448+
isObject && parameterInput.end_date
449+
? parameterInput.end_date
450+
: defaultEnd;
451+
const paramGranularity =
452+
isObject && parameterInput.granularity
453+
? parameterInput.granularity
454+
: dynamicRequest.granularity;
455+
456+
const validation = validateParameterRequest(
457+
parameterName,
458+
siteId,
459+
paramStart,
460+
paramEnd
461+
);
462+
if (!validation.success) {
463+
return {
464+
parameter: customId,
465+
success: false,
466+
error: validation.error,
467+
data: [],
468+
};
469+
}
457470

458-
try {
459-
const queryRequest = {
460-
projectId: validation.siteId,
461-
type: parameterName,
462-
from: validation.start,
463-
to: validation.end,
464-
timeUnit: getTimeUnit(
465-
paramGranularity,
466-
validation.start,
467-
validation.end
468-
),
469-
filters: dynamicRequest.filters || [],
470-
limit: dynamicRequest.limit || 100,
471-
offset: dynamicRequest.page
472-
? (dynamicRequest.page - 1) * (dynamicRequest.limit || 100)
473-
: 0,
474-
timezone: params.timezone,
475-
};
471+
try {
472+
const queryRequest = {
473+
projectId: validation.siteId,
474+
type: parameterName,
475+
from: validation.start,
476+
to: validation.end,
477+
timeUnit: getTimeUnit(
478+
paramGranularity,
479+
validation.start,
480+
validation.end
481+
),
482+
filters: dynamicRequest.filters || [],
483+
limit: dynamicRequest.limit || 100,
484+
offset: dynamicRequest.page
485+
? (dynamicRequest.page - 1) * (dynamicRequest.limit || 100)
486+
: 0,
487+
timezone: params.timezone,
488+
};
476489

477-
const data = await executeQuery(queryRequest, domain, params.timezone);
490+
const data = await record("query.execute_metric", () => {
491+
setAttributes({
492+
"query.metric": parameterName,
493+
"query.metric.site_id": validation.siteId,
494+
"query.metric.custom_id": customId,
495+
"query.metric.time_range.start": validation.start,
496+
"query.metric.time_range.end": validation.end,
497+
});
498+
return executeQuery(queryRequest, domain, params.timezone);
499+
});
478500

479-
return {
480-
parameter: customId,
481-
success: true,
482-
data: data || [],
483-
};
484-
} catch (error) {
485-
return {
486-
parameter: customId,
487-
success: false,
488-
error: error instanceof Error ? error.message : "Query failed",
489-
data: [],
490-
};
501+
return {
502+
parameter: customId,
503+
success: true,
504+
data: data || [],
505+
};
506+
} catch (error) {
507+
return {
508+
parameter: customId,
509+
success: false,
510+
error: error instanceof Error ? error.message : "Query failed",
511+
data: [],
512+
};
513+
}
491514
}
492-
}
493515

494-
const parameterResults = await Promise.all(
495-
request.parameters.map((param) =>
496-
processParameter(
497-
param,
498-
request,
499-
queryParams,
500-
websiteId,
501-
startDate,
502-
endDate,
503-
websiteDomain
516+
const parameterResults = await Promise.all(
517+
request.parameters.map((param) =>
518+
processParameter(
519+
param,
520+
request,
521+
queryParams,
522+
websiteId,
523+
startDate,
524+
endDate,
525+
websiteDomain
526+
)
504527
)
505-
)
506-
);
528+
);
507529

508-
return {
509-
queryId: request.id,
510-
data: parameterResults,
511-
meta: {
512-
parameters: request.parameters,
513-
total_parameters: request.parameters.length,
514-
page: request.page || 1,
515-
limit: request.limit || 100,
516-
filters_applied: request.filters?.length || 0,
517-
},
518-
};
530+
return {
531+
queryId: request.id,
532+
data: parameterResults,
533+
meta: {
534+
parameters: request.parameters,
535+
total_parameters: request.parameters.length,
536+
page: request.page || 1,
537+
limit: request.limit || 100,
538+
filters_applied: request.filters?.length || 0,
539+
},
540+
};
541+
});
519542
}

apps/docs/components/features.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export default function Features() {
131131
<div
132132
className={cn(
133133
"flex transform-gpu flex-col justify-center border-border border-t-[1.2px] border-l-[1.2px] p-10 md:min-h-[240px] md:border-t-0"
134-
)}
134+
)}
135135
key={item.id}
136136
>
137137
<div className="my-1 flex items-center gap-2">

0 commit comments

Comments
 (0)