Route legacy report APIs through compatibility handlers

This commit is contained in:
Mike Cao committed 2026-09-09 23:11:55 -07:00
1 parent 6c8d065e21
commit 667d718047
40 files changed
+94 -5155

No files matched your search

+9 -5
View File
@@ -82,10 +82,15 @@ Section-based sharing is preserved. Heatmaps remain authenticated-only. New defi
support type-specific section sharing and exclude deleted websites. Legacy report list routes
retain their original, differing authentication behavior during the transition.
## Compatibility and removal gate
## Compatibility routing and removal gate
All legacy calculation and saved-report routes remain callable with their existing contracts.
OpenAPI marks them deprecated; generated SDK methods carry `@deprecated` and remain available.
Next.js rewrites the old URLs to handlers under `src/app/(compat)/compat/api` without
changing request methods, bodies, query parameters, or response shapes. These handlers live
outside `src/app/api`, so contract discovery, OpenAPI, and the generated SDK expose only the
new feature APIs. Existing clients can continue calling the old URLs; new SDK versions use
the replacements above. HTTP redirects to the feature APIs would not preserve compatibility
because the methods, request envelopes, and some response shapes differ.
Legacy handlers and new feature routes call the same SQL functions; no internal HTTP forwarding
is involved. Legacy calculation envelopes and date precedence remain unchanged.
@@ -99,14 +104,13 @@ Before scheduling removal:
5. Announce a compatibility window and the breaking release that will remove legacy routes.
6. Verify the application, boards, SDK examples, and MCP no longer call legacy endpoints.
No removal date or version is scheduled by this change. The metadata and documentation prepare
the deprecation; they do not announce a release externally or delete historical records.
No removal date or version is scheduled by this change. Historical records are preserved.
## Validation
Unit tests cover GET serialization, invalid structured input, normalized ranges, permission
checks, definition website/type mismatches, immutable update scope, SQL dataset separation for
PostgreSQL and ClickHouse, cache keys, SDK requests, MCP composition, and deprecation metadata.
PostgreSQL and ClickHouse, cache keys, SDK requests, MCP composition, and compatibility routing.
`tests/api/report-migration.spec.ts` exercises legacy/new result parity, saved-definition
lifecycles, shared access, and split datasets against the existing disposable API test stack.
+9
View File
@@ -236,6 +236,15 @@ export default withNextIntl({
async rewrites() {
return [
...rewrites,
// Preserve legacy report requests outside the published API contract.
{
source: '/api/reports/:path*',
destination: '/compat/api/reports/:path*',
},
{
source: '/api/websites/:websiteId/reports',
destination: '/compat/api/websites/:websiteId/reports',
},
{
source: '/telemetry.js',
destination: '/api/scripts/telemetry',
+1 -1
View File
@@ -45,7 +45,7 @@ They are deprecated and will be removed in the next major release. The object fo
## Method names
Method names are OpenAPI `operationId`s. Most match the old names (`getWebsiteStats`,
`runFunnelReport`, `getRealtime`, `createTeam`, …). Endpoints that were not in the old client are
`getWebsiteFunnelStats`, `getRealtime`, `createTeam`, …). Endpoints that were not in the old client are
now available automatically. Endpoints under `/api/admin`, `/api/config`, `/api/dashboard` and
other internal routes are intentionally excluded.
+9 -6
View File
@@ -68,16 +68,19 @@ describe('UmamiClient', () => {
test('sends JSON bodies for POST operations', async () => {
const { client, calls } = createClient([]);
await client.runFunnelReport({
await client.createWebsiteFunnel({
websiteId: 'w1',
type: 'funnel',
filters: {},
parameters: { startDate: 'a', endDate: 'b', window: 60, steps: [] },
name: 'Signup funnel',
parameters: { window: 60, steps: [] },
});
expect(calls[0].method).toBe('POST');
expect(calls[0].url.pathname).toBe('/api/reports/funnel');
expect(JSON.parse(calls[0].body as string)).toMatchObject({ websiteId: 'w1', type: 'funnel' });
expect(calls[0].url.pathname).toBe('/api/websites/w1/funnels');
expect(JSON.parse(calls[0].body as string)).toEqual({
websiteId: 'w1',
name: 'Signup funnel',
parameters: { window: 60, steps: [] },
});
});
test('sends structured funnel criteria through the GET stats API', async () => {
-18
View File
@@ -504,22 +504,4 @@ export class UmamiClient extends GeneratedUmamiClient {
return super.getWebsiteSessionProperties(first, second as RequestOptions | undefined);
}
override getWebsiteReports(
input: Input<'getWebsiteReports'>,
options?: RequestOptions,
): Output<'getWebsiteReports'>;
override getWebsiteReports(
websiteId: string,
params?: Params<'getWebsiteReports', 'websiteId'>,
options?: RequestOptions,
): Output<'getWebsiteReports'>;
override getWebsiteReports(
first: string | Input<'getWebsiteReports'>,
second?: Record<string, unknown> | RequestOptions,
third?: RequestOptions,
) {
const [input, options] = positional<'getWebsiteReports'>('websiteId', first, second, third);
return super.getWebsiteReports(input, options);
}
}
@@ -106,14 +106,6 @@ export const operations = {
queryParams: [],
hasBody: true,
},
createReport: {
operationId: 'createReport',
method: 'post',
path: '/api/reports',
pathParams: [],
queryParams: [],
hasBody: true,
},
createShare: {
operationId: 'createShare',
method: 'post',
@@ -226,14 +218,6 @@ export const operations = {
queryParams: [],
hasBody: false,
},
deleteReport: {
operationId: 'deleteReport',
method: 'delete',
path: '/api/reports/{reportId}',
pathParams: ['reportId'],
queryParams: [],
hasBody: false,
},
deleteShare: {
operationId: 'deleteShare',
method: 'delete',
@@ -1055,22 +1039,6 @@ export const operations = {
],
hasBody: false,
},
getReport: {
operationId: 'getReport',
method: 'get',
path: '/api/reports/{reportId}',
pathParams: ['reportId'],
queryParams: [],
hasBody: false,
},
getReports: {
operationId: 'getReports',
method: 'get',
path: '/api/reports',
pathParams: [],
queryParams: ['websiteId', 'type', 'page', 'pageSize', 'maxResults'],
hasBody: false,
},
getSessionDataArraySeries: {
operationId: 'getSessionDataArraySeries',
method: 'get',
@@ -2307,14 +2275,6 @@ export const operations = {
queryParams: [],
hasBody: false,
},
getWebsiteReports: {
operationId: 'getWebsiteReports',
method: 'get',
path: '/api/websites/{websiteId}/reports',
pathParams: ['websiteId'],
queryParams: ['type', 'page', 'pageSize', 'maxResults'],
hasBody: false,
},
getWebsiteRetention: {
operationId: 'getWebsiteRetention',
method: 'get',
@@ -2995,86 +2955,6 @@ export const operations = {
queryParams: [],
hasBody: false,
},
runAttributionReport: {
operationId: 'runAttributionReport',
method: 'post',
path: '/api/reports/attribution',
pathParams: [],
queryParams: [],
hasBody: true,
},
runBreakdownReport: {
operationId: 'runBreakdownReport',
method: 'post',
path: '/api/reports/breakdown',
pathParams: [],
queryParams: [],
hasBody: true,
},
runFunnelReport: {
operationId: 'runFunnelReport',
method: 'post',
path: '/api/reports/funnel',
pathParams: [],
queryParams: [],
hasBody: true,
},
runGoalReport: {
operationId: 'runGoalReport',
method: 'post',
path: '/api/reports/goal',
pathParams: [],
queryParams: [],
hasBody: true,
},
runHeatmapReport: {
operationId: 'runHeatmapReport',
method: 'post',
path: '/api/reports/heatmap',
pathParams: [],
queryParams: [],
hasBody: true,
},
runJourneyReport: {
operationId: 'runJourneyReport',
method: 'post',
path: '/api/reports/journey',
pathParams: [],
queryParams: [],
hasBody: true,
},
runPerformanceReport: {
operationId: 'runPerformanceReport',
method: 'post',
path: '/api/reports/performance',
pathParams: [],
queryParams: [],
hasBody: true,
},
runRetentionReport: {
operationId: 'runRetentionReport',
method: 'post',
path: '/api/reports/retention',
pathParams: [],
queryParams: [],
hasBody: true,
},
runRevenueReport: {
operationId: 'runRevenueReport',
method: 'post',
path: '/api/reports/revenue',
pathParams: [],
queryParams: [],
hasBody: true,
},
runUtmReport: {
operationId: 'runUtmReport',
method: 'post',
path: '/api/reports/utm',
pathParams: [],
queryParams: [],
hasBody: true,
},
saveWebsiteReplay: {
operationId: 'saveWebsiteReplay',
method: 'post',
@@ -3139,14 +3019,6 @@ export const operations = {
queryParams: [],
hasBody: true,
},
updateReport: {
operationId: 'updateReport',
method: 'post',
path: '/api/reports/{reportId}',
pathParams: ['reportId'],
queryParams: [],
hasBody: true,
},
updateShare: {
operationId: 'updateShare',
method: 'post',
@@ -3409,21 +3281,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('createPixelShare', input, options);
}
/**
* Save a report
* @deprecated See the operation description for migration details.
* Saves a report's name, description, type, and parameters for a website so it can be opened again later.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `POST /api/reports`
*/
createReport(
input: OperationInput<'createReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'createReport'>> {
return this.execute('createReport', input, options);
}
/**
* Create a share
* Creates a named share for a website, board, link, or pixel with parameters and an optional custom slug.
@@ -3590,21 +3447,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('deletePixel', input, options);
}
/**
* Delete a saved report
* @deprecated See the operation description for migration details.
* Deletes the specified saved report definition.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `DELETE /api/reports/{reportId}`
*/
deleteReport(
input: OperationInput<'deleteReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'deleteReport'>> {
return this.execute('deleteReport', input, options);
}
/**
* Delete a share
* Deletes the specified share after checking permission to delete shares for its resource.
@@ -4095,36 +3937,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('getRealtime', input, options);
}
/**
* Get a saved report
* @deprecated See the operation description for migration details.
* Returns the specified report's saved name, description, type, and parameters.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `GET /api/reports/{reportId}`
*/
getReport(
input: OperationInput<'getReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'getReport'>> {
return this.execute('getReport', input, options);
}
/**
* List saved reports
* @deprecated See the operation description for migration details.
* Returns a paginated list of saved reports for the requested website, optionally filtered by report type.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `GET /api/reports`
*/
getReports(
input: OperationInput<'getReports'>,
options?: RequestOptions,
): Promise<OperationOutput<'getReports'>> {
return this.execute('getReports', input, options);
}
/**
* Get session array values over time
* Counts distinct sessions for individual values in an array property, grouped by value and time interval.
@@ -4736,21 +4548,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('getWebsiteReplaySaved', input, options);
}
/**
* List a website's saved reports
* @deprecated See the operation description for migration details.
* Returns a paginated list of saved report definitions for the website, optionally filtered by report type.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `GET /api/websites/{websiteId}/reports`
*/
getWebsiteReports(
input: OperationInput<'getWebsiteReports'>,
options?: RequestOptions,
): Promise<OperationOutput<'getWebsiteReports'>> {
return this.execute('getWebsiteReports', input, options);
}
/**
* Get website retention
* `GET /api/websites/{websiteId}/retention`
@@ -5095,156 +4892,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('resetWebsite', input, options);
}
/**
* Run an attribution report
* @deprecated See the operation description for migration details.
* Calculates how traffic sources contribute to conversions using the supplied attribution settings, date range, and filters.
*
* Deprecated: use GET /api/websites/{websiteId}/attribution. The legacy request and response remain supported during migration.
* `POST /api/reports/attribution`
*/
runAttributionReport(
input: OperationInput<'runAttributionReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runAttributionReport'>> {
return this.execute('runAttributionReport', input, options);
}
/**
* Run a breakdown report
* @deprecated See the operation description for migration details.
* Groups website activity by the selected dimensions for the requested date range and filters.
*
* Deprecated: use GET /api/websites/{websiteId}/breakdown. The legacy request and response remain supported during migration.
* `POST /api/reports/breakdown`
*/
runBreakdownReport(
input: OperationInput<'runBreakdownReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runBreakdownReport'>> {
return this.execute('runBreakdownReport', input, options);
}
/**
* Run a funnel report
* @deprecated See the operation description for migration details.
* Calculates visitor progression through a sequence of pages or events using the supplied funnel steps and filters.
*
* Deprecated: use GET /api/websites/{websiteId}/funnels/stats or /funnels/{funnelId}/stats. The legacy request and response remain supported during migration.
* `POST /api/reports/funnel`
*/
runFunnelReport(
input: OperationInput<'runFunnelReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runFunnelReport'>> {
return this.execute('runFunnelReport', input, options);
}
/**
* Run a goal report
* @deprecated See the operation description for migration details.
* Counts visitors who reached a matching page or triggered a matching event and returns the total visitor count for comparison.
*
* Deprecated: use GET /api/websites/{websiteId}/goals/stats or /goals/{goalId}/stats. The legacy request and response remain supported during migration.
* `POST /api/reports/goal`
*/
runGoalReport(
input: OperationInput<'runGoalReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runGoalReport'>> {
return this.execute('runGoalReport', input, options);
}
/**
* Get page heatmap data
* @deprecated See the operation description for migration details.
* Returns recorded click or scroll data for the selected page and date range to display as a heatmap.
*
* Deprecated: use GET /api/websites/{websiteId}/heatmaps. The legacy request and response remain supported during migration.
* `POST /api/reports/heatmap`
*/
runHeatmapReport(
input: OperationInput<'runHeatmapReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runHeatmapReport'>> {
return this.execute('runHeatmapReport', input, options);
}
/**
* Run a visitor journey report
* @deprecated See the operation description for migration details.
* Returns paths through pages or events using the supplied journey settings and website filters.
*
* Deprecated: use GET /api/websites/{websiteId}/journeys. The legacy request and response remain supported during migration.
* `POST /api/reports/journey`
*/
runJourneyReport(
input: OperationInput<'runJourneyReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runJourneyReport'>> {
return this.execute('runJourneyReport', input, options);
}
/**
* Run a performance report
* @deprecated See the operation description for migration details.
* Returns performance trends, summary metrics, and breakdowns by page, page title, device, and browser for the selected website and date range.
*
* Deprecated: use GET /api/websites/{websiteId}/performance/stats, /performance/chart, and /performance/metrics. The legacy request and response remain supported during migration.
* `POST /api/reports/performance`
*/
runPerformanceReport(
input: OperationInput<'runPerformanceReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runPerformanceReport'>> {
return this.execute('runPerformanceReport', input, options);
}
/**
* Run a retention report
* @deprecated See the operation description for migration details.
* Calculates how groups of visitors return over time using the supplied retention settings, date range, and filters.
*
* Deprecated: use GET /api/websites/{websiteId}/retention. The legacy request and response remain supported during migration.
* `POST /api/reports/retention`
*/
runRetentionReport(
input: OperationInput<'runRetentionReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runRetentionReport'>> {
return this.execute('runRetentionReport', input, options);
}
/**
* Run a revenue report
* @deprecated See the operation description for migration details.
* Returns revenue trends, totals with a comparison period, and breakdowns by country, region, referrer, and channel.
*
* Deprecated: use GET /api/websites/{websiteId}/revenue/stats, /revenue/chart, and /revenue/metrics. The legacy request and response remain supported during migration.
* `POST /api/reports/revenue`
*/
runRevenueReport(
input: OperationInput<'runRevenueReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runRevenueReport'>> {
return this.execute('runRevenueReport', input, options);
}
/**
* Run a campaign report
* @deprecated See the operation description for migration details.
* Returns traffic breakdowns for UTM source, medium, campaign, term, and content using the supplied date range and filters.
*
* Deprecated: use GET /api/websites/{websiteId}/utm/metrics for each UTM dimension. The legacy request and response remain supported during migration.
* `POST /api/reports/utm`
*/
runUtmReport(
input: OperationInput<'runUtmReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'runUtmReport'>> {
return this.execute('runUtmReport', input, options);
}
/**
* Save or unsave a session replay
* Adds a replay to the website's saved replays with an optional name, or removes it when isSaved is false.
@@ -5335,21 +4982,6 @@ export abstract class GeneratedUmamiClient {
return this.execute('updatePixel', input, options);
}
/**
* Update a saved report
* @deprecated See the operation description for migration details.
* Updates the specified report's website, name, description, type, and parameters.
*
* Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.
* `POST /api/reports/{reportId}`
*/
updateReport(
input: OperationInput<'updateReport'>,
options?: RequestOptions,
): Promise<OperationOutput<'updateReport'>> {
return this.execute('updateReport', input, options);
}
/**
* Update a share
* Updates the specified share's name, slug, and parameters.
File diff suppressed because it is too large. Load diff
+9 -10
View File
@@ -19,11 +19,11 @@ const statsOperation: OperationDefinition = {
hasBody: false,
};
const reportOperation: OperationDefinition = {
operationId: 'runFunnelReport',
const funnelOperation: OperationDefinition = {
operationId: 'createWebsiteFunnel',
method: 'post',
path: '/api/reports/funnel',
pathParams: [],
path: '/api/websites/{websiteId}/funnels',
pathParams: ['websiteId'],
queryParams: [],
hasBody: true,
};
@@ -111,16 +111,15 @@ describe('splitInput', () => {
test('routes unknown keys to the body for operations with a request body', () => {
expect(
splitInput(reportOperation, {
splitInput(funnelOperation, {
websiteId: 'w',
type: 'funnel',
name: 'Signup',
parameters: { steps: [] },
filters: {},
}),
).toEqual({
path: {},
path: { websiteId: 'w' },
query: {},
body: { websiteId: 'w', type: 'funnel', parameters: { steps: [] }, filters: {} },
body: { websiteId: 'w', name: 'Signup', parameters: { steps: [] } },
});
});
});
@@ -143,7 +142,7 @@ describe('sendRequest', () => {
const result = await sendRequest(fetchImpl, {
method: 'post',
url: new URL('https://example.com/api/reports/funnel'),
url: new URL('https://example.com/api/websites/w/funnels'),
headers: { authorization: 'Bearer t', 'x-custom': '1' },
body: { websiteId: 'w' },
});
File renamed without changes.
@@ -1,453 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'delete',
path: '/api/reports/{reportId}',
audience: 'public',
auth: 'bearer',
operation: {
operationId: 'deleteReport',
summary: 'Delete reports report id',
tags: ['Reports'],
parameters: [
{
name: 'reportId',
in: 'path',
required: true,
schema: {
type: 'string',
},
},
],
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
ok: {
const: true,
},
},
required: ['ok'],
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'404': {
description: 'Not found.',
content: {
'application/json': {
example: {
error: {
message: 'Not found.',
code: 'not-found',
status: 404,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
const operation2 = defineOperation({
method: 'get',
path: '/api/reports/{reportId}',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'getReport',
summary: 'Get reports report id',
tags: ['Reports'],
parameters: [
{
name: 'reportId',
in: 'path',
required: true,
schema: {
type: 'string',
},
},
],
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
userId: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
updatedAt: {
type: 'string',
format: 'date-time',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
parameters: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{
type: 'object',
properties: {},
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
websiteId: {
type: 'string',
},
},
required: [
'id',
'userId',
'createdAt',
'updatedAt',
'name',
'type',
'description',
'parameters',
'websiteId',
],
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'404': {
description: 'Not found.',
content: {
'application/json': {
example: {
error: {
message: 'Not found.',
code: 'not-found',
status: 404,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
const operation3 = defineOperation({
method: 'post',
path: '/api/reports/{reportId}',
audience: 'public',
auth: 'bearer',
operation: {
operationId: 'updateReport',
summary: 'Create or update reports report id',
tags: ['Reports'],
parameters: [
{
name: 'reportId',
in: 'path',
required: true,
schema: {
type: 'string',
},
},
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
type: {
type: 'string',
},
name: {
type: 'string',
maxLength: 200,
},
description: {
type: 'string',
maxLength: 500,
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'type', 'name', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
userId: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
updatedAt: {
type: 'string',
format: 'date-time',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
parameters: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{
type: 'object',
properties: {},
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
websiteId: {
type: 'string',
},
},
required: [
'id',
'userId',
'createdAt',
'updatedAt',
'name',
'type',
'description',
'parameters',
'websiteId',
],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'404': {
description: 'Not found.',
content: {
'application/json': {
example: {
error: {
message: 'Not found.',
code: 'not-found',
status: 404,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1, operation2, operation3] as const;
@@ -1,224 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/attribution',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runAttributionReport',
summary: 'Create or update reports attribution',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
referrer: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
paidAds: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
utm_source: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
utm_medium: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
utm_campaign: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
utm_content: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
utm_term: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
total: {
type: 'object',
properties: {
pageviews: {
type: 'number',
},
visitors: {
type: 'number',
},
visits: {
type: 'number',
},
},
required: ['pageviews', 'visitors', 'visits'],
},
},
required: [
'referrer',
'paidAds',
'utm_source',
'utm_medium',
'utm_campaign',
'utm_content',
'utm_term',
'total',
],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,123 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/breakdown',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runBreakdownReport',
summary: 'Create or update reports breakdown',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
type: 'object',
properties: {
views: {
type: 'number',
},
visitors: {
type: 'number',
},
visits: {
type: 'number',
},
bounces: {
type: 'number',
},
totaltime: {
type: 'number',
},
},
required: ['views', 'visitors', 'visits', 'bounces', 'totaltime'],
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
],
},
},
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
-439
View File
@@ -1,439 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'get',
path: '/api/reports',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'getReports',
summary: 'Get reports',
tags: ['Reports'],
parameters: [
{
name: 'websiteId',
in: 'query',
required: true,
schema: {
type: 'string',
format: 'uuid',
},
},
{
name: 'type',
in: 'query',
required: false,
schema: {
type: 'string',
},
},
{
name: 'page',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
{
name: 'pageSize',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
{
name: 'maxResults',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
],
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'array',
items: {
allOf: [
{
type: 'object',
properties: {
id: {
type: 'string',
},
userId: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
updatedAt: {
type: 'string',
format: 'date-time',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
parameters: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{
type: 'object',
properties: {},
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
websiteId: {
type: 'string',
},
},
required: [
'id',
'userId',
'createdAt',
'updatedAt',
'name',
'type',
'description',
'parameters',
'websiteId',
],
},
{
type: 'object',
properties: {
website: {
type: 'object',
properties: {
domain: {
type: 'string',
},
userId: {
type: 'string',
},
},
required: ['domain', 'userId'],
},
},
},
],
},
},
count: {
type: 'number',
},
page: {
type: 'number',
},
pageSize: {
type: 'number',
},
orderBy: {
type: 'string',
},
sortDescending: {
type: 'boolean',
},
search: {
type: 'string',
},
isCapped: {
type: 'boolean',
},
},
required: ['data', 'count', 'page', 'pageSize'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
const operation2 = defineOperation({
method: 'post',
path: '/api/reports',
audience: 'public',
auth: 'bearer',
operation: {
operationId: 'createReport',
summary: 'Create or update reports',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
type: {
type: 'string',
},
name: {
type: 'string',
maxLength: 200,
},
description: {
type: 'string',
maxLength: 500,
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'type', 'name', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
userId: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
updatedAt: {
type: 'string',
format: 'date-time',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
parameters: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{
type: 'object',
properties: {},
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
websiteId: {
type: 'string',
},
},
required: [
'id',
'userId',
'createdAt',
'updatedAt',
'name',
'type',
'description',
'parameters',
'websiteId',
],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1, operation2] as const;
@@ -1,145 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/funnel',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runFunnelReport',
summary: 'Create or update reports funnel',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
type: 'object',
properties: {
visitors: {
type: 'number',
},
previous: {
type: 'number',
},
dropped: {
type: 'number',
},
dropoff: {
type: 'number',
},
remaining: {
type: 'number',
},
type: {
type: 'string',
},
value: {
type: 'string',
},
filters: {
type: 'array',
items: {
type: 'object',
properties: {
property: {
type: 'string',
},
operator: {
type: 'string',
},
value: {
type: 'string',
},
},
required: ['property', 'operator', 'value'],
},
},
},
required: [
'visitors',
'previous',
'dropped',
'dropoff',
'remaining',
'type',
'value',
],
},
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,101 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/goal',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runGoalReport',
summary: 'Create or update reports goal',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
num: {
type: 'number',
},
total: {
type: 'number',
},
},
required: ['num', 'total'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,251 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/heatmap',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runHeatmapReport',
summary: 'Create or update reports heatmap',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mode: {
anyOf: [
{
const: 'click',
},
{
const: 'scroll',
},
],
},
pages: {
type: 'array',
items: {
type: 'object',
properties: {
urlPath: {
type: 'string',
},
count: {
type: 'number',
},
sessions: {
type: 'number',
},
},
required: ['urlPath', 'count', 'sessions'],
},
},
points: {
type: 'array',
items: {
type: 'object',
properties: {
x: {
type: 'number',
},
y: {
type: 'number',
},
pageX: {
type: 'number',
},
pageY: {
type: 'number',
},
pageW: {
type: 'number',
},
pageH: {
type: 'number',
},
viewportW: {
type: 'number',
},
viewportH: {
type: 'number',
},
count: {
type: 'number',
},
},
required: [
'x',
'y',
'pageX',
'pageY',
'pageW',
'pageH',
'viewportW',
'viewportH',
'count',
],
},
},
snapshot: {
type: 'object',
properties: {
kind: {
const: 'iframe',
},
id: {
type: 'string',
},
url: {
type: 'string',
},
pageW: {
type: 'number',
},
pageH: {
type: 'number',
},
viewportW: {
type: 'number',
},
viewportH: {
type: 'number',
},
},
required: ['kind', 'id', 'url', 'pageW', 'pageH', 'viewportW', 'viewportH'],
},
scroll: {
type: 'object',
properties: {
buckets: {
type: 'array',
items: {
type: 'object',
properties: {
depth: {
type: 'number',
},
sessions: {
type: 'number',
},
pageW: {
type: 'number',
},
pageH: {
type: 'number',
},
viewportW: {
type: 'number',
},
viewportH: {
type: 'number',
},
},
required: ['depth', 'sessions', 'pageW', 'pageH', 'viewportW', 'viewportH'],
},
},
totalSessions: {
type: 'number',
},
pageW: {
type: 'number',
},
pageH: {
type: 'number',
},
viewportW: {
type: 'number',
},
viewportH: {
type: 'number',
},
},
required: [
'buckets',
'totalSessions',
'pageW',
'pageH',
'viewportW',
'viewportH',
],
},
},
required: ['mode', 'pages', 'points', 'snapshot', 'scroll'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,122 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/journey',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runJourneyReport',
summary: 'Create or update reports journey',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
type: 'object',
properties: {
e1: {
type: 'string',
},
e2: {
type: 'string',
},
e3: {
type: 'string',
},
e4: {
type: 'string',
},
e5: {
type: 'string',
},
e6: {
type: 'string',
},
e7: {
type: 'string',
},
count: {
type: 'number',
},
},
required: ['e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'count'],
},
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,101 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/performance',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runPerformanceReport',
summary: 'Create or update reports performance',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
chart: {},
summary: {},
pages: {},
pageTitles: {},
devices: {},
browsers: {},
},
required: ['chart', 'summary', 'pages', 'pageTitles', 'devices', 'browsers'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,113 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/retention',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runRetentionReport',
summary: 'Create or update reports retention',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
type: 'object',
properties: {
date: {
type: 'string',
},
day: {
type: 'number',
},
visitors: {
type: 'number',
},
returnVisitors: {
type: 'number',
},
percentage: {
type: 'number',
},
},
required: ['date', 'day', 'visitors', 'returnVisitors', 'percentage'],
},
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,180 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/revenue',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runRevenueReport',
summary: 'Create or update reports revenue',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
chart: {
type: 'array',
items: {
type: 'object',
properties: {
x: {
type: 'string',
},
t: {
type: 'string',
},
y: {
type: 'number',
},
count: {
type: 'number',
},
},
required: ['x', 't', 'y', 'count'],
},
},
total: {},
country: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
region: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
country: {
type: 'string',
},
},
required: ['name', 'value', 'country'],
},
},
referrer: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
channel: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
value: {
type: 'number',
},
},
required: ['name', 'value'],
},
},
},
required: ['chart', 'total', 'country', 'region', 'referrer', 'channel'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,115 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'post',
path: '/api/reports/utm',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'runUtmReport',
summary: 'Create or update reports utm',
tags: ['Reports'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
websiteId: {
type: 'string',
format: 'uuid',
},
filters: {
type: 'object',
additionalProperties: true,
},
type: {
type: 'string',
},
parameters: {
type: 'object',
additionalProperties: true,
},
},
required: ['websiteId', 'filters', 'type', 'parameters'],
},
},
},
},
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
utm_source: {
type: 'array',
items: {},
},
utm_medium: {
type: 'array',
items: {},
},
utm_campaign: {
type: 'array',
items: {},
},
utm_term: {
type: 'array',
items: {},
},
utm_content: {
type: 'array',
items: {},
},
},
required: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
@@ -1,254 +0,0 @@
// Generated by `pnpm openapi:contracts`. Do not edit directly.
import type { ZodOpenApiOperationObject } from 'zod-openapi';
import { defineOperation } from '@/openapi/operation';
const operation1 = defineOperation({
method: 'get',
path: '/api/websites/{websiteId}/reports',
audience: 'public',
auth: 'bearer-or-share',
operation: {
operationId: 'getWebsiteReports',
summary: 'Get websites website id reports',
tags: ['Websites'],
parameters: [
{
name: 'websiteId',
in: 'path',
required: true,
schema: {
type: 'string',
},
},
{
name: 'type',
in: 'query',
required: false,
schema: {
type: 'string',
},
},
{
name: 'page',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
{
name: 'pageSize',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
{
name: 'maxResults',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
},
},
],
responses: {
'200': {
description: 'The operation completed successfully.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'array',
items: {
allOf: [
{
type: 'object',
properties: {
id: {
type: 'string',
},
userId: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
updatedAt: {
type: 'string',
format: 'date-time',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
parameters: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{
type: 'object',
properties: {},
additionalProperties: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
const: false,
},
{
const: true,
},
{},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
},
{
type: 'object',
properties: {
length: {
type: 'number',
},
},
required: ['length'],
},
],
},
websiteId: {
type: 'string',
},
},
required: [
'id',
'userId',
'createdAt',
'updatedAt',
'name',
'type',
'description',
'parameters',
'websiteId',
],
},
{
type: 'object',
properties: {
website: {
type: 'object',
properties: {
domain: {
type: 'string',
},
userId: {
type: 'string',
},
},
required: ['domain', 'userId'],
},
},
},
],
},
},
count: {
type: 'number',
},
page: {
type: 'number',
},
pageSize: {
type: 'number',
},
orderBy: {
type: 'string',
},
sortDescending: {
type: 'boolean',
},
search: {
type: 'string',
},
isCapped: {
type: 'boolean',
},
},
required: ['data', 'count', 'page', 'pageSize'],
},
},
},
},
'400': {
description: 'Bad request.',
content: {
'application/json': {
example: {
error: {
message: 'Bad request.',
code: 'bad-request',
status: 400,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
'401': {
description: 'Unauthorized.',
content: {
'application/json': {
example: {
error: {
message: 'Unauthorized.',
code: 'unauthorized',
status: 401,
},
},
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
},
},
} as ZodOpenApiOperationObject,
});
export const operations = [operation1] as const;
+57
View File
@@ -0,0 +1,57 @@
import {
getRewrittenUrl,
isRewrite,
unstable_getResponseFromNextConfig,
} from 'next/experimental/testing/server';
import { describe, expect, test } from 'vitest';
import nextConfig from '../../next.config';
describe('legacy report compatibility routing', () => {
test.each([
'/api/reports',
'/api/reports/saved-report-id',
...[
'attribution',
'breakdown',
'funnel',
'goal',
'heatmap',
'journey',
'performance',
'retention',
'revenue',
'utm',
].map(type => `/api/reports/${type}`),
'/api/websites/website-id/reports',
])('rewrites %s while preserving query parameters', async path => {
const query = '?websiteId=website-id&type=funnel&page=2';
const response = await unstable_getResponseFromNextConfig({
url: `https://example.com${path}${query}`,
nextConfig: { ...nextConfig, basePath: '' },
});
expect(isRewrite(response)).toBe(true);
expect(response.headers.get('location')).toBeNull();
expect(getRewrittenUrl(response)).toBe(`https://example.com/compat${path}${query}`);
});
test('leaves feature routes unchanged', async () => {
const response = await unstable_getResponseFromNextConfig({
url: 'https://example.com/api/websites/website-id/funnels',
nextConfig: { ...nextConfig, basePath: '' },
});
expect(isRewrite(response)).toBe(false);
});
test('preserves a configured base path', async () => {
const response = await unstable_getResponseFromNextConfig({
url: 'https://example.com/umami/api/reports/funnel?websiteId=website-id',
nextConfig: { ...nextConfig, basePath: '/umami' },
});
expect(getRewrittenUrl(response)).toBe(
'https://example.com/umami/compat/api/reports/funnel?websiteId=website-id',
);
});
});
-8
View File
@@ -12,7 +12,6 @@ import { applyFieldDescriptions } from '@/openapi/field-descriptions';
import { inferApiContracts } from '@/openapi/infer';
import { type ApiAudience, getOperationKey } from '@/openapi/operation';
import { operationDescriptions } from '@/openapi/operation-descriptions';
import { getReportDeprecation } from '@/openapi/report-deprecations';
import { errorResponseComponents } from '@/openapi/schemas';
import { getSecurityRequirements, securitySchemes } from '@/openapi/security';
@@ -87,16 +86,9 @@ export async function buildOpenApiDocument(
paths[contract.path] = pathItem;
}
const deprecation = getReportDeprecation(contract.method, contract.path);
const description =
operationDescriptions[getOperationKey(contract)]?.description ??
contract.operation.description;
const operation = {
...contract.operation,
...operationDescriptions[getOperationKey(contract)],
...(deprecation
? { deprecated: true, description: [description, deprecation].filter(Boolean).join('\n\n') }
: {}),
security: getSecurityRequirements(contract.auth),
'x-umami-audience': contract.audience,
'x-umami-contract': contract.origin,
-77
View File
@@ -277,78 +277,6 @@ export const operationDescriptions: Record<string, OperationDescription> = {
description:
'Stores session replay events or heatmap clicks and scrolls for a website, using a valid tracking cache token to identify the session and visit.',
},
'GET /api/reports': {
summary: 'List saved reports',
description:
'Returns a paginated list of saved reports for the requested website, optionally filtered by report type.',
},
'POST /api/reports': {
summary: 'Save a report',
description:
"Saves a report's name, description, type, and parameters for a website so it can be opened again later.",
},
'DELETE /api/reports/{reportId}': {
summary: 'Delete a saved report',
description: 'Deletes the specified saved report definition.',
},
'GET /api/reports/{reportId}': {
summary: 'Get a saved report',
description: "Returns the specified report's saved name, description, type, and parameters.",
},
'POST /api/reports/{reportId}': {
summary: 'Update a saved report',
description: "Updates the specified report's website, name, description, type, and parameters.",
},
'POST /api/reports/attribution': {
summary: 'Run an attribution report',
description:
'Calculates how traffic sources contribute to conversions using the supplied attribution settings, date range, and filters.',
},
'POST /api/reports/breakdown': {
summary: 'Run a breakdown report',
description:
'Groups website activity by the selected dimensions for the requested date range and filters.',
},
'POST /api/reports/funnel': {
summary: 'Run a funnel report',
description:
'Calculates visitor progression through a sequence of pages or events using the supplied funnel steps and filters.',
},
'POST /api/reports/goal': {
summary: 'Run a goal report',
description:
'Counts visitors who reached a matching page or triggered a matching event and returns the total visitor count for comparison.',
},
'POST /api/reports/heatmap': {
summary: 'Get page heatmap data',
description:
'Returns recorded click or scroll data for the selected page and date range to display as a heatmap.',
},
'POST /api/reports/journey': {
summary: 'Run a visitor journey report',
description:
'Returns paths through pages or events using the supplied journey settings and website filters.',
},
'POST /api/reports/performance': {
summary: 'Run a performance report',
description:
'Returns performance trends, summary metrics, and breakdowns by page, page title, device, and browser for the selected website and date range.',
},
'POST /api/reports/retention': {
summary: 'Run a retention report',
description:
'Calculates how groups of visitors return over time using the supplied retention settings, date range, and filters.',
},
'POST /api/reports/revenue': {
summary: 'Run a revenue report',
description:
'Returns revenue trends, totals with a comparison period, and breakdowns by country, region, referrer, and channel.',
},
'POST /api/reports/utm': {
summary: 'Run a campaign report',
description:
'Returns traffic breakdowns for UTM source, medium, campaign, term, and content using the supplied date range and filters.',
},
'GET /api/scripts/telemetry': {
summary: 'Get the installation telemetry script',
description:
@@ -661,11 +589,6 @@ export const operationDescriptions: Record<string, OperationDescription> = {
description:
"Adds a replay to the website's saved replays with an optional name, or removes it when isSaved is false.",
},
'GET /api/websites/{websiteId}/reports': {
summary: "List a website's saved reports",
description:
'Returns a paginated list of saved report definitions for the website, optionally filtered by report type.',
},
'POST /api/websites/{websiteId}/reset': {
summary: 'Reset website analytics',
description:
-16
View File
@@ -34,21 +34,6 @@ export const OPERATION_ID_OVERRIDES: Record<string, string> = {
'GET /api/realtime/{websiteId}': 'getRealtime',
// Reports
'GET /api/reports': 'getReports',
'POST /api/reports': 'createReport',
'GET /api/reports/{reportId}': 'getReport',
'POST /api/reports/{reportId}': 'updateReport',
'DELETE /api/reports/{reportId}': 'deleteReport',
'POST /api/reports/attribution': 'runAttributionReport',
'POST /api/reports/breakdown': 'runBreakdownReport',
'POST /api/reports/funnel': 'runFunnelReport',
'POST /api/reports/goal': 'runGoalReport',
'POST /api/reports/heatmap': 'runHeatmapReport',
'POST /api/reports/journey': 'runJourneyReport',
'POST /api/reports/performance': 'runPerformanceReport',
'POST /api/reports/retention': 'runRetentionReport',
'POST /api/reports/revenue': 'runRevenueReport',
'POST /api/reports/utm': 'runUtmReport',
// Teams
'POST /api/teams': 'createTeam',
@@ -118,7 +103,6 @@ export const OPERATION_ID_OVERRIDES: Record<string, string> = {
'GET /api/websites/{websiteId}/export': 'exportWebsite',
'POST /api/websites/{websiteId}/reset': 'resetWebsite',
'POST /api/websites/{websiteId}/transfer': 'transferWebsite',
'GET /api/websites/{websiteId}/reports': 'getWebsiteReports',
'GET /api/websites/{websiteId}/shares': 'getWebsiteShares',
'POST /api/websites/{websiteId}/shares': 'createWebsiteShare',
'GET /api/websites/{websiteId}/recorder': 'getWebsiteRecorderConfig',
-34
View File
@@ -1,34 +0,0 @@
import { expect, test } from 'vitest';
import { getReportDeprecation } from './report-deprecations';
test('marks all legacy calculation and persistence routes with replacements', () => {
for (const type of [
'attribution',
'breakdown',
'funnel',
'goal',
'heatmap',
'journey',
'performance',
'retention',
'revenue',
'utm',
]) {
expect(getReportDeprecation('post', `/api/reports/${type}`)).toContain(
'GET /api/websites/{websiteId}/',
);
}
expect(getReportDeprecation('get', '/api/reports')).toContain('persisted-data audit');
expect(getReportDeprecation('delete', '/api/reports/{reportId}')).toContain(
'Existing IDs are preserved',
);
expect(getReportDeprecation('get', '/api/websites/{websiteId}/reports')).toContain(
'no removal date',
);
});
test('does not deprecate the replacement feature APIs', () => {
expect(getReportDeprecation('get', '/api/websites/{websiteId}/funnels/stats')).toBeUndefined();
expect(getReportDeprecation('post', '/api/websites/{websiteId}/funnels')).toBeUndefined();
expect(getReportDeprecation('get', '/api/websites/{websiteId}/revenue/stats')).toBeUndefined();
});
-27
View File
@@ -1,27 +0,0 @@
/** Keep the legacy surface discoverable until an announced breaking release. */
const calculations: Record<string, string> = {
attribution: 'GET /api/websites/{websiteId}/attribution',
breakdown: 'GET /api/websites/{websiteId}/breakdown',
funnel: 'GET /api/websites/{websiteId}/funnels/stats or /funnels/{funnelId}/stats',
goal: 'GET /api/websites/{websiteId}/goals/stats or /goals/{goalId}/stats',
heatmap: 'GET /api/websites/{websiteId}/heatmaps',
journey: 'GET /api/websites/{websiteId}/journeys',
performance:
'GET /api/websites/{websiteId}/performance/stats, /performance/chart, and /performance/metrics',
retention: 'GET /api/websites/{websiteId}/retention',
revenue: 'GET /api/websites/{websiteId}/revenue/stats, /revenue/chart, and /revenue/metrics',
utm: 'GET /api/websites/{websiteId}/utm/metrics for each UTM dimension',
};
export function getReportDeprecation(method: string, path: string): string | undefined {
const feature = path.startsWith('/api/reports/') ? path.slice('/api/reports/'.length) : '';
const replacement = method === 'post' ? calculations[feature] : undefined;
if (replacement) {
return `Deprecated: use ${replacement}. The legacy request and response remain supported during migration.`;
}
if (
['/api/reports', '/api/reports/{reportId}', '/api/websites/{websiteId}/reports'].includes(path)
) {
return 'Deprecated: use website-scoped /funnels or /goals resources for saved definitions. Existing IDs are preserved. Other legacy report types remain accessible here pending a persisted-data audit; no removal date has been scheduled.';
}
}