docs(api): deprecate report routes and document migration compatibility
This commit is contained in:
1 parent
7c4406bad0
commit
4fbaa5300d
46 files changed
+564
-72
No files matched your search
@@ -0,0 +1,118 @@
|
|||||||
|
# Report API migration
|
||||||
|
|
||||||
|
The website analytics APIs replace report execution endpoints. Analytics use GET; saved
|
||||||
|
funnels and goals have dedicated resource routes. The application and MCP use the new routes.
|
||||||
|
The report table and existing definition IDs are retained. No database migration is required.
|
||||||
|
|
||||||
|
## Calculation endpoints
|
||||||
|
|
||||||
|
Paths in the replacement column are relative to `/api/websites/{websiteId}`.
|
||||||
|
|
||||||
|
| Legacy POST endpoint | Replacement GET endpoint(s) |
|
||||||
|
| --- | --- |
|
||||||
|
| `/api/reports/attribution` | `/attribution` |
|
||||||
|
| `/api/reports/breakdown` | `/breakdown` |
|
||||||
|
| `/api/reports/funnel` | `/funnels/stats` or `/funnels/{funnelId}/stats` |
|
||||||
|
| `/api/reports/goal` | `/goals/stats` or `/goals/{goalId}/stats` |
|
||||||
|
| `/api/reports/heatmap` | `/heatmaps` |
|
||||||
|
| `/api/reports/journey` | `/journeys` |
|
||||||
|
| `/api/reports/performance` | `/performance/stats`, `/performance/chart`, `/performance/metrics` |
|
||||||
|
| `/api/reports/retention` | `/retention` |
|
||||||
|
| `/api/reports/revenue` | Existing `/revenue/stats`, `/revenue/chart`, `/revenue/metrics` |
|
||||||
|
| `/api/reports/utm` | `/utm/metrics` |
|
||||||
|
|
||||||
|
New requests use `startAt` and `endAt` in epoch milliseconds, with optional `timezone`, `unit`,
|
||||||
|
and existing filter parameters. Dates are normalized through the shared website filter pipeline,
|
||||||
|
including reset dates and account history limits. The report envelope and discriminator are gone.
|
||||||
|
Use a single date range; do not send separate filter dates and calculation dates.
|
||||||
|
|
||||||
|
`steps` on funnel previews and `fields` on breakdowns are JSON strings, URL-encoded normally.
|
||||||
|
Other criteria are individual query parameters. For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await client.getWebsiteFunnelStats({
|
||||||
|
websiteId,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
timezone: 'UTC',
|
||||||
|
window: 60,
|
||||||
|
steps: JSON.stringify([
|
||||||
|
{ type: 'path', value: '/pricing' },
|
||||||
|
{ type: 'event', value: 'signup' },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The calculation criteria remain feature-specific: journeys accept `steps`, `startStep`,
|
||||||
|
`endStep`, and `eventType`; attribution accepts `model`, `type`, and `step`; goal previews
|
||||||
|
accept `type` and `value`; heatmaps accept `urlPath` and `mode`. See `/openapi.json` for contracts.
|
||||||
|
|
||||||
|
Performance stats return the old `summary` object directly. Performance chart returns
|
||||||
|
`{ chart }`. Performance metrics return a single dimension array; `type` is `path`, `title`,
|
||||||
|
`device`, or `browser`. Chart and metrics accept `metric` (`lcp`, `inp`, `cls`, `fcp`, `ttfb`).
|
||||||
|
Stats calculate all metrics independently of the chart selection. The same percentile SQL is
|
||||||
|
used by legacy and new handlers.
|
||||||
|
|
||||||
|
UTM metrics return `{ utm, views }[]` for one `type`: `utm_source`, `utm_medium`, `utm_campaign`,
|
||||||
|
`utm_term`, or `utm_content`. The existing pageview counting rules, empty-value exclusion,
|
||||||
|
and 50-row limit are preserved. UTM board widgets request only their configured dimension.
|
||||||
|
|
||||||
|
Revenue endpoints retain their existing contracts. MCP `get_revenue` composes stats, chart,
|
||||||
|
and four dimension requests, preserving its previous output and comparison data.
|
||||||
|
|
||||||
|
## Saved definitions
|
||||||
|
|
||||||
|
| Method | Path (relative to website) | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/funnels` | List definitions, with pagination/search |
|
||||||
|
| POST | `/funnels` | Create `{ name, description?, parameters }` |
|
||||||
|
| GET | `/funnels/{funnelId}` | Read definition |
|
||||||
|
| POST | `/funnels/{funnelId}` | Update `{ name, description?, parameters }` |
|
||||||
|
| DELETE | `/funnels/{funnelId}` | Delete definition |
|
||||||
|
| GET | `/funnels/{funnelId}/stats` | Calculate saved criteria with supplied dates/filters |
|
||||||
|
| GET | `/funnels/stats` | Calculate supplied criteria without saving |
|
||||||
|
|
||||||
|
Goals expose the same structure under `/goals`. Website and definition type are fixed by the
|
||||||
|
route and cannot be changed in an update body. IDs, names, descriptions, and board `reportId`
|
||||||
|
references are preserved. Saved stats validate stored criteria and require both definition access
|
||||||
|
and analytics-section access. Unsupported or malformed legacy criteria return 400 for stats;
|
||||||
|
the definition remains readable/editable through the compatibility API.
|
||||||
|
|
||||||
|
Section-based sharing is preserved. Heatmaps remain authenticated-only. New definition lists
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
Do not remove generic report CRUD solely because the current UI only saves funnels and goals.
|
||||||
|
Before scheduling removal:
|
||||||
|
|
||||||
|
1. Audit persisted report counts by type, including older definitions outside funnel/goal.
|
||||||
|
2. Audit malformed funnel/goal criteria that stricter feature APIs cannot calculate.
|
||||||
|
3. Decide how users can export, edit, migrate, or retain every other saved type.
|
||||||
|
4. Confirm external API clients have replacements for their operations and payloads.
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
It is intended to run in both database modes. Do not point it at a development or production
|
||||||
|
database: the API suite seeds and mutates test data. This migration was developed without a
|
||||||
|
running disposable API server; live backend parity remains a release verification step.
|
||||||
|
|
||||||
|
Regenerate and verify contracts with `pnpm generate:api`, `pnpm openapi:check`, and
|
||||||
|
`pnpm check:api:client`. Build packages before their tests so MCP resolves the current API client.
|
||||||
@@ -97,7 +97,7 @@ describe('UmamiClient', () => {
|
|||||||
expect(calls[0].method).toBe('GET');
|
expect(calls[0].method).toBe('GET');
|
||||||
expect(calls[0].body).toBeUndefined();
|
expect(calls[0].body).toBeUndefined();
|
||||||
expect(calls[0].url.pathname).toBe('/api/websites/w1/funnels/stats');
|
expect(calls[0].url.pathname).toBe('/api/websites/w1/funnels/stats');
|
||||||
expect(JSON.parse(calls[0].url.searchParams.get('steps')!)).toEqual(steps);
|
expect(JSON.parse(calls[0].url.searchParams.get('steps') ?? 'null')).toEqual(steps);
|
||||||
expect(calls[0].url.searchParams.get('browser1')).toBe('eq.Chrome');
|
expect(calls[0].url.searchParams.get('browser1')).toBe('eq.Chrome');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3411,7 +3411,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a report
|
* 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.
|
* 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`
|
* `POST /api/reports`
|
||||||
*/
|
*/
|
||||||
createReport(
|
createReport(
|
||||||
@@ -3589,7 +3592,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a saved report
|
* Delete a saved report
|
||||||
|
* @deprecated See the operation description for migration details.
|
||||||
* Deletes the specified saved report definition.
|
* 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}`
|
* `DELETE /api/reports/{reportId}`
|
||||||
*/
|
*/
|
||||||
deleteReport(
|
deleteReport(
|
||||||
@@ -4091,7 +4097,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a saved report
|
* Get a saved report
|
||||||
|
* @deprecated See the operation description for migration details.
|
||||||
* Returns the specified report's saved name, description, type, and parameters.
|
* 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}`
|
* `GET /api/reports/{reportId}`
|
||||||
*/
|
*/
|
||||||
getReport(
|
getReport(
|
||||||
@@ -4103,7 +4112,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List saved reports
|
* 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.
|
* 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`
|
* `GET /api/reports`
|
||||||
*/
|
*/
|
||||||
getReports(
|
getReports(
|
||||||
@@ -4726,7 +4738,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List a website's saved reports
|
* 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.
|
* 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`
|
* `GET /api/websites/{websiteId}/reports`
|
||||||
*/
|
*/
|
||||||
getWebsiteReports(
|
getWebsiteReports(
|
||||||
@@ -5082,7 +5097,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run an attribution report
|
* 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.
|
* 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`
|
* `POST /api/reports/attribution`
|
||||||
*/
|
*/
|
||||||
runAttributionReport(
|
runAttributionReport(
|
||||||
@@ -5094,7 +5112,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a breakdown report
|
* 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.
|
* 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`
|
* `POST /api/reports/breakdown`
|
||||||
*/
|
*/
|
||||||
runBreakdownReport(
|
runBreakdownReport(
|
||||||
@@ -5106,7 +5127,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a funnel report
|
* 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.
|
* 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`
|
* `POST /api/reports/funnel`
|
||||||
*/
|
*/
|
||||||
runFunnelReport(
|
runFunnelReport(
|
||||||
@@ -5118,7 +5142,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a goal report
|
* 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.
|
* 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`
|
* `POST /api/reports/goal`
|
||||||
*/
|
*/
|
||||||
runGoalReport(
|
runGoalReport(
|
||||||
@@ -5130,7 +5157,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page heatmap data
|
* 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.
|
* 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`
|
* `POST /api/reports/heatmap`
|
||||||
*/
|
*/
|
||||||
runHeatmapReport(
|
runHeatmapReport(
|
||||||
@@ -5142,7 +5172,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a visitor journey report
|
* 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.
|
* 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`
|
* `POST /api/reports/journey`
|
||||||
*/
|
*/
|
||||||
runJourneyReport(
|
runJourneyReport(
|
||||||
@@ -5154,7 +5187,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a performance report
|
* 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.
|
* 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`
|
* `POST /api/reports/performance`
|
||||||
*/
|
*/
|
||||||
runPerformanceReport(
|
runPerformanceReport(
|
||||||
@@ -5166,7 +5202,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a retention report
|
* 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.
|
* 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`
|
* `POST /api/reports/retention`
|
||||||
*/
|
*/
|
||||||
runRetentionReport(
|
runRetentionReport(
|
||||||
@@ -5178,7 +5217,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a revenue report
|
* 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.
|
* 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`
|
* `POST /api/reports/revenue`
|
||||||
*/
|
*/
|
||||||
runRevenueReport(
|
runRevenueReport(
|
||||||
@@ -5190,7 +5232,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a campaign report
|
* 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.
|
* 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`
|
* `POST /api/reports/utm`
|
||||||
*/
|
*/
|
||||||
runUtmReport(
|
runUtmReport(
|
||||||
@@ -5292,7 +5337,10 @@ export abstract class GeneratedUmamiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a saved report
|
* Update a saved report
|
||||||
|
* @deprecated See the operation description for migration details.
|
||||||
* Updates the specified report's website, name, description, type, and parameters.
|
* 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}`
|
* `POST /api/reports/{reportId}`
|
||||||
*/
|
*/
|
||||||
updateReport(
|
updateReport(
|
||||||
|
|||||||
@@ -895,13 +895,19 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* List saved reports
|
* List saved reports
|
||||||
|
* @deprecated
|
||||||
* @description Returns a paginated list of saved reports for the requested website, optionally filtered by report type.
|
* @description 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: operations['getReports'];
|
get: operations['getReports'];
|
||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Save a report
|
* Save a report
|
||||||
|
* @deprecated
|
||||||
* @description Saves a report's name, description, type, and parameters for a website so it can be opened again later.
|
* @description 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: operations['createReport'];
|
post: operations['createReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -919,18 +925,27 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Get a saved report
|
* Get a saved report
|
||||||
|
* @deprecated
|
||||||
* @description Returns the specified report's saved name, description, type, and parameters.
|
* @description 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: operations['getReport'];
|
get: operations['getReport'];
|
||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Update a saved report
|
* Update a saved report
|
||||||
|
* @deprecated
|
||||||
* @description Updates the specified report's website, name, description, type, and parameters.
|
* @description 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: operations['updateReport'];
|
post: operations['updateReport'];
|
||||||
/**
|
/**
|
||||||
* Delete a saved report
|
* Delete a saved report
|
||||||
|
* @deprecated
|
||||||
* @description Deletes the specified saved report definition.
|
* @description 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: operations['deleteReport'];
|
delete: operations['deleteReport'];
|
||||||
options?: never;
|
options?: never;
|
||||||
@@ -949,7 +964,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run an attribution report
|
* Run an attribution report
|
||||||
|
* @deprecated
|
||||||
* @description Calculates how traffic sources contribute to conversions using the supplied attribution settings, date range, and filters.
|
* @description 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: operations['runAttributionReport'];
|
post: operations['runAttributionReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -969,7 +987,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a breakdown report
|
* Run a breakdown report
|
||||||
|
* @deprecated
|
||||||
* @description Groups website activity by the selected dimensions for the requested date range and filters.
|
* @description 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: operations['runBreakdownReport'];
|
post: operations['runBreakdownReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -989,7 +1010,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a funnel report
|
* Run a funnel report
|
||||||
|
* @deprecated
|
||||||
* @description Calculates visitor progression through a sequence of pages or events using the supplied funnel steps and filters.
|
* @description 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: operations['runFunnelReport'];
|
post: operations['runFunnelReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1009,7 +1033,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a goal report
|
* Run a goal report
|
||||||
|
* @deprecated
|
||||||
* @description Counts visitors who reached a matching page or triggered a matching event and returns the total visitor count for comparison.
|
* @description 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: operations['runGoalReport'];
|
post: operations['runGoalReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1029,7 +1056,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Get page heatmap data
|
* Get page heatmap data
|
||||||
|
* @deprecated
|
||||||
* @description Returns recorded click or scroll data for the selected page and date range to display as a heatmap.
|
* @description 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: operations['runHeatmapReport'];
|
post: operations['runHeatmapReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1049,7 +1079,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a visitor journey report
|
* Run a visitor journey report
|
||||||
|
* @deprecated
|
||||||
* @description Returns paths through pages or events using the supplied journey settings and website filters.
|
* @description 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: operations['runJourneyReport'];
|
post: operations['runJourneyReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1069,7 +1102,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a performance report
|
* Run a performance report
|
||||||
|
* @deprecated
|
||||||
* @description Returns performance trends, summary metrics, and breakdowns by page, page title, device, and browser for the selected website and date range.
|
* @description 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: operations['runPerformanceReport'];
|
post: operations['runPerformanceReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1089,7 +1125,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a retention report
|
* Run a retention report
|
||||||
|
* @deprecated
|
||||||
* @description Calculates how groups of visitors return over time using the supplied retention settings, date range, and filters.
|
* @description 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: operations['runRetentionReport'];
|
post: operations['runRetentionReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1109,7 +1148,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a revenue report
|
* Run a revenue report
|
||||||
|
* @deprecated
|
||||||
* @description Returns revenue trends, totals with a comparison period, and breakdowns by country, region, referrer, and channel.
|
* @description 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: operations['runRevenueReport'];
|
post: operations['runRevenueReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -1129,7 +1171,10 @@ export interface paths {
|
|||||||
put?: never;
|
put?: never;
|
||||||
/**
|
/**
|
||||||
* Run a campaign report
|
* Run a campaign report
|
||||||
|
* @deprecated
|
||||||
* @description Returns traffic breakdowns for UTM source, medium, campaign, term, and content using the supplied date range and filters.
|
* @description 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: operations['runUtmReport'];
|
post: operations['runUtmReport'];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -2456,7 +2501,10 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* List a website's saved reports
|
* List a website's saved reports
|
||||||
|
* @deprecated
|
||||||
* @description Returns a paginated list of saved report definitions for the website, optionally filtered by report type.
|
* @description 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: operations['getWebsiteReports'];
|
get: operations['getWebsiteReports'];
|
||||||
put?: never;
|
put?: never;
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ describe('createUmamiMcpServer', () => {
|
|||||||
expect(init?.method).toBe('GET');
|
expect(init?.method).toBe('GET');
|
||||||
expect(init?.body).toBeUndefined();
|
expect(init?.body).toBeUndefined();
|
||||||
expect(url.pathname).toBe(`/api/websites/${WEBSITE_ID}/funnels/stats`);
|
expect(url.pathname).toBe(`/api/websites/${WEBSITE_ID}/funnels/stats`);
|
||||||
expect(JSON.parse(url.searchParams.get('steps')!)).toHaveLength(2);
|
expect(JSON.parse(url.searchParams.get('steps') ?? 'null')).toHaveLength(2);
|
||||||
expect(url.searchParams.get('window')).toBe('60');
|
expect(url.searchParams.get('window')).toBe('60');
|
||||||
expect(Number(url.searchParams.get('startAt'))).toBeGreaterThan(0);
|
expect(Number(url.searchParams.get('startAt'))).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface OpenApiOperation {
|
|||||||
operationId?: string;
|
operationId?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
deprecated?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
parameters?: OpenApiParameter[];
|
parameters?: OpenApiParameter[];
|
||||||
requestBody?: { required?: boolean };
|
requestBody?: { required?: boolean };
|
||||||
@@ -55,6 +56,7 @@ interface OperationDefinition {
|
|||||||
path: string;
|
path: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
deprecated?: boolean;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
pathParams: string[];
|
pathParams: string[];
|
||||||
queryParams: string[];
|
queryParams: string[];
|
||||||
@@ -119,6 +121,7 @@ function collectOperations(document: OpenApiDocument): OperationDefinition[] {
|
|||||||
path: route,
|
path: route,
|
||||||
summary: operation.summary ?? operationId,
|
summary: operation.summary ?? operationId,
|
||||||
description: operation.description,
|
description: operation.description,
|
||||||
|
deprecated: operation.deprecated,
|
||||||
tags: operation.tags ?? [],
|
tags: operation.tags ?? [],
|
||||||
pathParams,
|
pathParams,
|
||||||
queryParams,
|
queryParams,
|
||||||
@@ -137,7 +140,7 @@ function collectOperations(document: OpenApiDocument): OperationDefinition[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jsDoc(lines: (string | undefined)[]) {
|
function jsDoc(lines: (string | undefined)[]) {
|
||||||
const content = lines.filter((line): line is string => !!line);
|
const content = lines.filter((line): line is string => !!line).flatMap(line => line.split('\n'));
|
||||||
|
|
||||||
if (!content.length) {
|
if (!content.length) {
|
||||||
return '';
|
return '';
|
||||||
@@ -170,6 +173,9 @@ function renderOperations(operations: OperationDefinition[], version: string) {
|
|||||||
!operation.bodyRequired;
|
!operation.bodyRequired;
|
||||||
const doc = jsDoc([
|
const doc = jsDoc([
|
||||||
operation.summary,
|
operation.summary,
|
||||||
|
operation.deprecated
|
||||||
|
? '@deprecated See the operation description for migration details.'
|
||||||
|
: undefined,
|
||||||
operation.description ? '' : undefined,
|
operation.description ? '' : undefined,
|
||||||
operation.description,
|
operation.description,
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useModified } from '@/components/hooks/useModified';
|
|
||||||
import { Button, Column, ListItem, Row, Select, Text, TextField } from '@umami/react-zen';
|
import { Button, Column, ListItem, Row, Select, Text, TextField } from '@umami/react-zen';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useApi, useMessages } from '@/components/hooks';
|
import { useApi, useMessages } from '@/components/hooks';
|
||||||
|
import { useModified } from '@/components/hooks/useModified';
|
||||||
import { LinkSelect } from '@/components/input/LinkSelect';
|
import { LinkSelect } from '@/components/input/LinkSelect';
|
||||||
import { PixelSelect } from '@/components/input/PixelSelect';
|
import { PixelSelect } from '@/components/input/PixelSelect';
|
||||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { ListTable } from '@/components/metrics/ListTable';
|
|||||||
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
||||||
import { PerformanceCard } from '@/components/metrics/PerformanceCard';
|
import { PerformanceCard } from '@/components/metrics/PerformanceCard';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
import { CHART_COLORS, WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
import { CHART_COLORS } from '@/lib/constants';
|
||||||
import { generateTimeSeries } from '@/lib/date';
|
import { generateTimeSeries } from '@/lib/date';
|
||||||
import { formatLongNumber } from '@/lib/format';
|
import { formatLongNumber } from '@/lib/format';
|
||||||
import styles from './Performance.module.css';
|
import styles from './Performance.module.css';
|
||||||
@@ -140,7 +140,6 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance
|
|||||||
|
|
||||||
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
|
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
|
||||||
|
|
||||||
const threshold = WEB_VITALS_THRESHOLDS[selectedMetric as keyof typeof WEB_VITALS_THRESHOLDS];
|
|
||||||
const isCls = selectedMetric === 'cls';
|
const isCls = selectedMetric === 'cls';
|
||||||
const metricLabel = t(labels[selectedMetric]) || selectedMetric.toUpperCase();
|
const metricLabel = t(labels[selectedMetric]) || selectedMetric.toUpperCase();
|
||||||
const formatListCount = isCls
|
const formatListCount = isCls
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
|
|||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { reportResultSchema } from '@/lib/schema';
|
import { reportResultSchema } from '@/lib/schema';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
import { type AttributionParameters, getAttribution } from '@/queries/sql/attribution/getAttribution';
|
import {
|
||||||
|
type AttributionParameters,
|
||||||
|
getAttribution,
|
||||||
|
} from '@/queries/sql/attribution/getAttribution';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const { auth, body, error } = await parseRequest(request, reportResultSchema);
|
const { auth, body, error } = await parseRequest(request, reportResultSchema);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
|
|||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { reportResultSchema } from '@/lib/schema';
|
import { reportResultSchema } from '@/lib/schema';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
import { getPerformance, type PerformanceParameters } from '@/queries/sql/performance/getPerformance';
|
import {
|
||||||
|
getPerformance,
|
||||||
|
type PerformanceParameters,
|
||||||
|
} from '@/queries/sql/performance/getPerformance';
|
||||||
import { getPerformanceMetrics } from '@/queries/sql/performance/getPerformanceMetrics';
|
import { getPerformanceMetrics } from '@/queries/sql/performance/getPerformanceMetrics';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { attributionQuerySchema } from '@/lib/analytics-schema';
|
import { attributionQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { attributionQuerySchema } from '@/lib/analytics-schema';
|
|||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
import { getAttribution, type AttributionParameters } from '@/queries/sql/attribution/getAttribution';
|
import {
|
||||||
|
type AttributionParameters,
|
||||||
|
getAttribution,
|
||||||
|
} from '@/queries/sql/attribution/getAttribution';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { breakdownQuerySchema } from '@/lib/analytics-schema';
|
import { breakdownQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { breakdownQuerySchema } from '@/lib/analytics-schema';
|
|||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
import { getBreakdown, type BreakdownParameters } from '@/queries/sql/breakdown/getBreakdown';
|
import { type BreakdownParameters, getBreakdown } from '@/queries/sql/breakdown/getBreakdown';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { funnelDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
import { defineOperation } from '@/openapi/operation';
|
||||||
import {
|
import {
|
||||||
badRequestResponse,
|
badRequestResponse,
|
||||||
unauthorizedResponse,
|
|
||||||
notFoundResponse,
|
|
||||||
jsonResponse,
|
jsonResponse,
|
||||||
|
notFoundResponse,
|
||||||
okSchema,
|
okSchema,
|
||||||
|
unauthorizedResponse,
|
||||||
} from '@/openapi/schemas';
|
} from '@/openapi/schemas';
|
||||||
import { funnelDefinitionSchema } from '@/lib/analytics-schema';
|
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { funnelDefinitionSchema } from '@/lib/analytics-schema';
|
import { funnelDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
||||||
import { canViewReport, canUpdateReport, canDeleteReport } from '@/permissions';
|
import { canDeleteReport, canUpdateReport, canViewReport } from '@/permissions';
|
||||||
import { getReport, updateReport, deleteReport } from '@/queries/prisma';
|
import { deleteReport, getReport, updateReport } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { savedStatsQuerySchema } from '@/lib/analytics-schema';
|
import { savedStatsQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { savedStatsQuerySchema, funnelParametersSchema } from '@/lib/analytics-schema';
|
import { funnelParametersSchema, savedStatsQuerySchema } from '@/lib/analytics-schema';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized, notFound, badRequest } from '@/lib/response';
|
import { badRequest, json, notFound, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection, canViewReport } from '@/permissions';
|
import { canViewReport, canViewWebsiteSection } from '@/permissions';
|
||||||
import { getReport } from '@/queries/prisma';
|
import { getReport } from '@/queries/prisma';
|
||||||
import { getFunnel, type FunnelParameters } from '@/queries/sql/funnels/getFunnel';
|
import { type FunnelParameters, getFunnel } from '@/queries/sql/funnels/getFunnel';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { definitionListSchema, funnelDefinitionSchema } from '@/lib/analytics-schema';
|
import { definitionListSchema, funnelDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { funnelQuerySchema } from '@/lib/analytics-schema';
|
import { funnelQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
|||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
|
|
||||||
import { getFunnel, type FunnelParameters } from '@/queries/sql/funnels/getFunnel';
|
import { type FunnelParameters, getFunnel } from '@/queries/sql/funnels/getFunnel';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { goalDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
import { defineOperation } from '@/openapi/operation';
|
||||||
import {
|
import {
|
||||||
badRequestResponse,
|
badRequestResponse,
|
||||||
unauthorizedResponse,
|
|
||||||
notFoundResponse,
|
|
||||||
jsonResponse,
|
jsonResponse,
|
||||||
|
notFoundResponse,
|
||||||
okSchema,
|
okSchema,
|
||||||
|
unauthorizedResponse,
|
||||||
} from '@/openapi/schemas';
|
} from '@/openapi/schemas';
|
||||||
import { goalDefinitionSchema } from '@/lib/analytics-schema';
|
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { goalDefinitionSchema } from '@/lib/analytics-schema';
|
import { goalDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
||||||
import { canViewReport, canUpdateReport, canDeleteReport } from '@/permissions';
|
import { canDeleteReport, canUpdateReport, canViewReport } from '@/permissions';
|
||||||
import { getReport, updateReport, deleteReport } from '@/queries/prisma';
|
import { deleteReport, getReport, updateReport } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { savedStatsQuerySchema } from '@/lib/analytics-schema';
|
import { savedStatsQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { savedStatsQuerySchema, goalParametersSchema } from '@/lib/analytics-schema';
|
import { goalParametersSchema, savedStatsQuerySchema } from '@/lib/analytics-schema';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized, notFound, badRequest } from '@/lib/response';
|
import { badRequest, json, notFound, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection, canViewReport } from '@/permissions';
|
import { canViewReport, canViewWebsiteSection } from '@/permissions';
|
||||||
import { getReport } from '@/queries/prisma';
|
import { getReport } from '@/queries/prisma';
|
||||||
import { getGoal, type GoalParameters } from '@/queries/sql/goals/getGoal';
|
import { type GoalParameters, getGoal } from '@/queries/sql/goals/getGoal';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { definitionListSchema, goalDefinitionSchema } from '@/lib/analytics-schema';
|
import { definitionListSchema, goalDefinitionSchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { goalQuerySchema } from '@/lib/analytics-schema';
|
import { goalQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
|||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { canViewWebsiteSection } from '@/permissions';
|
import { canViewWebsiteSection } from '@/permissions';
|
||||||
|
|
||||||
import { getGoal, type GoalParameters } from '@/queries/sql/goals/getGoal';
|
import { type GoalParameters, getGoal } from '@/queries/sql/goals/getGoal';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { heatmapQuerySchema } from '@/lib/analytics-schema';
|
import { heatmapQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { journeyQuerySchema } from '@/lib/analytics-schema';
|
import { journeyQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { performanceChartQuerySchema } from '@/lib/analytics-schema';
|
import { performanceChartQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { performanceMetricsQuerySchema } from '@/lib/analytics-schema';
|
import { performanceMetricsQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { performanceStatsQuerySchema } from '@/lib/analytics-schema';
|
import { performanceStatsQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { retentionQuerySchema } from '@/lib/analytics-schema';
|
import { retentionQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineOperation } from '@/openapi/operation';
|
|
||||||
import { badRequestResponse, unauthorizedResponse, notFoundResponse } from '@/openapi/schemas';
|
|
||||||
import { utmMetricsQuerySchema } from '@/lib/analytics-schema';
|
import { utmMetricsQuerySchema } from '@/lib/analytics-schema';
|
||||||
|
import { defineOperation } from '@/openapi/operation';
|
||||||
|
import { badRequestResponse, notFoundResponse, unauthorizedResponse } from '@/openapi/schemas';
|
||||||
|
|
||||||
export const operations = [
|
export const operations = [
|
||||||
defineOperation({
|
defineOperation({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { expect, test, vi } from 'vitest';
|
|||||||
import { useFunnelQuery } from './useFunnelQuery';
|
import { useFunnelQuery } from './useFunnelQuery';
|
||||||
import { usePerformanceStatsQuery } from './usePerformanceStatsQuery';
|
import { usePerformanceStatsQuery } from './usePerformanceStatsQuery';
|
||||||
import { useUTMMetricsQuery } from './useUTMMetricsQuery';
|
import { useUTMMetricsQuery } from './useUTMMetricsQuery';
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({ get: vi.fn(), query: vi.fn((options: any) => options) }));
|
const mocks = vi.hoisted(() => ({ get: vi.fn(), query: vi.fn((options: any) => options) }));
|
||||||
vi.mock('../useApi', () => ({ useApi: () => ({ get: mocks.get, useQuery: mocks.query }) }));
|
vi.mock('../useApi', () => ({ useApi: () => ({ get: mocks.get, useQuery: mocks.query }) }));
|
||||||
vi.mock('../useDateParameters', () => ({
|
vi.mock('../useDateParameters', () => ({
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { JourneyParameters, JourneyResult } from '@/queries/sql/journeys/ge
|
|||||||
import { useAnalyticsQuery } from './useAnalyticsQuery';
|
import { useAnalyticsQuery } from './useAnalyticsQuery';
|
||||||
|
|
||||||
export function useJourneyQuery(
|
export function useJourneyQuery(
|
||||||
params: AnalyticsParameters & Partial<Omit<JourneyParameters, 'startDate' | 'endDate'>> & { eventType?: number },
|
params: AnalyticsParameters &
|
||||||
|
Partial<Omit<JourneyParameters, 'startDate' | 'endDate'>> & { eventType?: number },
|
||||||
options?: ReactQueryOptions<JourneyResult[]>,
|
options?: ReactQueryOptions<JourneyResult[]>,
|
||||||
) {
|
) {
|
||||||
return useAnalyticsQuery<JourneyResult[]>('journeys', params, options);
|
return useAnalyticsQuery<JourneyResult[]>('journeys', params, options);
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ vi.mock('@/queries/sql/breakdown/getBreakdown', () => ({ getBreakdown: mocks.que
|
|||||||
vi.mock('@/queries/sql/attribution/getAttribution', () => ({ getAttribution: mocks.query }));
|
vi.mock('@/queries/sql/attribution/getAttribution', () => ({ getAttribution: mocks.query }));
|
||||||
vi.mock('@/queries/sql/heatmap/getHeatmap', () => ({ getHeatmap: mocks.query }));
|
vi.mock('@/queries/sql/heatmap/getHeatmap', () => ({ getHeatmap: mocks.query }));
|
||||||
|
|
||||||
|
import { GET as attribution } from '@/app/api/websites/[websiteId]/attribution/route';
|
||||||
|
import { GET as breakdown } from '@/app/api/websites/[websiteId]/breakdown/route';
|
||||||
|
import { GET as heatmaps } from '@/app/api/websites/[websiteId]/heatmaps/route';
|
||||||
import { GET as journeys } from '@/app/api/websites/[websiteId]/journeys/route';
|
import { GET as journeys } from '@/app/api/websites/[websiteId]/journeys/route';
|
||||||
import { GET as retention } from '@/app/api/websites/[websiteId]/retention/route';
|
import { GET as retention } from '@/app/api/websites/[websiteId]/retention/route';
|
||||||
import { GET as breakdown } from '@/app/api/websites/[websiteId]/breakdown/route';
|
|
||||||
import { GET as attribution } from '@/app/api/websites/[websiteId]/attribution/route';
|
|
||||||
import { GET as heatmaps } from '@/app/api/websites/[websiteId]/heatmaps/route';
|
|
||||||
|
|
||||||
const cases = [
|
const cases = [
|
||||||
['journeys', journeys, { steps: '3' }],
|
['journeys', journeys, { steps: '3' }],
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
filterParams,
|
filterParams,
|
||||||
funnelReportSchema,
|
funnelReportSchema,
|
||||||
goalReportSchema,
|
goalReportSchema,
|
||||||
pagingParams,
|
|
||||||
heatmapReportSchema,
|
heatmapReportSchema,
|
||||||
journeyReportSchema,
|
journeyReportSchema,
|
||||||
|
pagingParams,
|
||||||
timezoneParam,
|
timezoneParam,
|
||||||
unitParam,
|
unitParam,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
const state = vi.hoisted(() => ({
|
const state = vi.hoisted(() => ({
|
||||||
report: null as any,
|
report: null as any,
|
||||||
sectionAllowed: true,
|
sectionAllowed: true,
|
||||||
@@ -33,10 +34,11 @@ vi.mock('@/lib/request', () => ({
|
|||||||
},
|
},
|
||||||
getQueryFilters: async () => ({ startDate: new Date(1500), endDate: new Date(2000) }),
|
getQueryFilters: async () => ({ startDate: new Date(1500), endDate: new Date(2000) }),
|
||||||
}));
|
}));
|
||||||
import * as goal from '@/app/api/websites/[websiteId]/goals/[goalId]/route';
|
|
||||||
import * as funnel from '@/app/api/websites/[websiteId]/funnels/[funnelId]/route';
|
import * as funnel from '@/app/api/websites/[websiteId]/funnels/[funnelId]/route';
|
||||||
import { GET as goalStats } from '@/app/api/websites/[websiteId]/goals/[goalId]/stats/route';
|
|
||||||
import { GET as funnelStats } from '@/app/api/websites/[websiteId]/funnels/[funnelId]/stats/route';
|
import { GET as funnelStats } from '@/app/api/websites/[websiteId]/funnels/[funnelId]/stats/route';
|
||||||
|
import * as goal from '@/app/api/websites/[websiteId]/goals/[goalId]/route';
|
||||||
|
import { GET as goalStats } from '@/app/api/websites/[websiteId]/goals/[goalId]/stats/route';
|
||||||
|
|
||||||
const context = { params: Promise.resolve({ websiteId: 'website', goalId: 'id', funnelId: 'id' }) };
|
const context = { params: Promise.resolve({ websiteId: 'website', goalId: 'id', funnelId: 'id' }) };
|
||||||
const request = () =>
|
const request = () =>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { applyFieldDescriptions } from '@/openapi/field-descriptions';
|
|||||||
import { inferApiContracts } from '@/openapi/infer';
|
import { inferApiContracts } from '@/openapi/infer';
|
||||||
import { type ApiAudience, getOperationKey } from '@/openapi/operation';
|
import { type ApiAudience, getOperationKey } from '@/openapi/operation';
|
||||||
import { operationDescriptions } from '@/openapi/operation-descriptions';
|
import { operationDescriptions } from '@/openapi/operation-descriptions';
|
||||||
|
import { getReportDeprecation } from '@/openapi/report-deprecations';
|
||||||
import { errorResponseComponents } from '@/openapi/schemas';
|
import { errorResponseComponents } from '@/openapi/schemas';
|
||||||
import { getSecurityRequirements, securitySchemes } from '@/openapi/security';
|
import { getSecurityRequirements, securitySchemes } from '@/openapi/security';
|
||||||
|
|
||||||
@@ -86,9 +87,16 @@ export async function buildOpenApiDocument(
|
|||||||
paths[contract.path] = pathItem;
|
paths[contract.path] = pathItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deprecation = getReportDeprecation(contract.method, contract.path);
|
||||||
|
const description =
|
||||||
|
operationDescriptions[getOperationKey(contract)]?.description ??
|
||||||
|
contract.operation.description;
|
||||||
const operation = {
|
const operation = {
|
||||||
...contract.operation,
|
...contract.operation,
|
||||||
...operationDescriptions[getOperationKey(contract)],
|
...operationDescriptions[getOperationKey(contract)],
|
||||||
|
...(deprecation
|
||||||
|
? { deprecated: true, description: [description, deprecation].filter(Boolean).join('\n\n') }
|
||||||
|
: {}),
|
||||||
security: getSecurityRequirements(contract.auth),
|
security: getSecurityRequirements(contract.auth),
|
||||||
'x-umami-audience': contract.audience,
|
'x-umami-audience': contract.audience,
|
||||||
'x-umami-contract': contract.origin,
|
'x-umami-contract': contract.origin,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/** 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.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './breakdown/getBreakdown';
|
||||||
export * from './events/getEventDataArraySeries';
|
export * from './events/getEventDataArraySeries';
|
||||||
export * from './events/getEventDataById';
|
export * from './events/getEventDataById';
|
||||||
export * from './events/getEventDataDateSeries';
|
export * from './events/getEventDataDateSeries';
|
||||||
@@ -17,19 +18,21 @@ export * from './events/getEventStats';
|
|||||||
export * from './events/getEventUsage';
|
export * from './events/getEventUsage';
|
||||||
export * from './events/getWebsiteEvents';
|
export * from './events/getWebsiteEvents';
|
||||||
export * from './events/saveEvent';
|
export * from './events/saveEvent';
|
||||||
|
export * from './funnels/getFunnel';
|
||||||
export * from './getActiveVisitors';
|
export * from './getActiveVisitors';
|
||||||
export * from './getChannelExpandedMetrics';
|
export * from './getChannelExpandedMetrics';
|
||||||
export * from './getChannelMetrics';
|
export * from './getChannelMetrics';
|
||||||
export * from './getRealtimeActivity';
|
export * from './getRealtimeActivity';
|
||||||
export * from './getRealtimeData';
|
export * from './getRealtimeData';
|
||||||
export * from './getWebsiteListCharts';
|
|
||||||
export * from './getValues';
|
export * from './getValues';
|
||||||
export * from './getWebsiteDateRange';
|
export * from './getWebsiteDateRange';
|
||||||
|
export * from './getWebsiteListCharts';
|
||||||
export * from './getWebsiteStats';
|
export * from './getWebsiteStats';
|
||||||
export * from './getWeeklyTraffic';
|
export * from './getWeeklyTraffic';
|
||||||
export * from './heatmap/extractHeatmapEvents';
|
export * from './heatmap/extractHeatmapEvents';
|
||||||
export * from './heatmap/getHeatmap';
|
export * from './heatmap/getHeatmap';
|
||||||
export * from './heatmap/saveHeatmapEvents';
|
export * from './heatmap/saveHeatmapEvents';
|
||||||
|
export * from './journeys/getJourney';
|
||||||
export * from './pageviews/getPageviewExpandedMetrics';
|
export * from './pageviews/getPageviewExpandedMetrics';
|
||||||
export * from './pageviews/getPageviewMetrics';
|
export * from './pageviews/getPageviewMetrics';
|
||||||
export * from './pageviews/getPageviewStats';
|
export * from './pageviews/getPageviewStats';
|
||||||
@@ -37,11 +40,7 @@ export * from './performance/getPerformanceStats';
|
|||||||
export * from './replays/getReplayChunks';
|
export * from './replays/getReplayChunks';
|
||||||
export * from './replays/getSessionReplays';
|
export * from './replays/getSessionReplays';
|
||||||
export * from './replays/saveRecording';
|
export * from './replays/saveRecording';
|
||||||
export * from './breakdown/getBreakdown';
|
|
||||||
export * from './funnels/getFunnel';
|
|
||||||
export * from './journeys/getJourney';
|
|
||||||
export * from './retention/getRetention';
|
export * from './retention/getRetention';
|
||||||
export * from './utm/getUTM';
|
|
||||||
export * from './sessions/createSession';
|
export * from './sessions/createSession';
|
||||||
export * from './sessions/getLinkedDistinctIds';
|
export * from './sessions/getLinkedDistinctIds';
|
||||||
export * from './sessions/getLinkedSessionIds';
|
export * from './sessions/getLinkedSessionIds';
|
||||||
@@ -65,3 +64,4 @@ export * from './sessions/getWebsiteSessions';
|
|||||||
export * from './sessions/saveSessionData';
|
export * from './sessions/saveSessionData';
|
||||||
export * from './sessions/saveSessionLink';
|
export * from './sessions/saveSessionLink';
|
||||||
export * from './sessions/updateSession';
|
export * from './sessions/updateSession';
|
||||||
|
export * from './utm/getUTM';
|
||||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|||||||
import { getPerformance } from './getPerformance';
|
import { getPerformance } from './getPerformance';
|
||||||
import { getPerformanceChart } from './getPerformanceChart';
|
import { getPerformanceChart } from './getPerformanceChart';
|
||||||
import { getPerformanceStats } from './getPerformanceStats';
|
import { getPerformanceStats } from './getPerformanceStats';
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
backend: 'prisma',
|
backend: 'prisma',
|
||||||
rawQuery: vi.fn(),
|
rawQuery: vi.fn(),
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { serializeAnalyticsQuery } from '../../src/lib/analytics-query';
|
||||||
|
import { expect, test } from './fixtures';
|
||||||
|
import { dateRange } from './helpers/dates';
|
||||||
|
import { uniqueName } from './helpers/entities';
|
||||||
|
|
||||||
|
const calculations = [
|
||||||
|
['journey', 'journeys', { steps: 3 }],
|
||||||
|
['retention', 'retention', {}],
|
||||||
|
['breakdown', 'breakdown', { fields: ['path', 'country'] }],
|
||||||
|
['attribution', 'attribution', { model: 'first-click', type: 'path', step: '/pricing' }],
|
||||||
|
['heatmap', 'heatmaps', { mode: 'click', urlPath: '/' }],
|
||||||
|
[
|
||||||
|
'funnel',
|
||||||
|
'funnels/stats',
|
||||||
|
{
|
||||||
|
window: 60,
|
||||||
|
steps: [
|
||||||
|
{ type: 'path', value: '/' },
|
||||||
|
{ type: 'path', value: '/pricing' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['goal', 'goals/stats', { type: 'path', value: '/' }],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// SQL rankings can have ties, so compare result content independently of tied row order.
|
||||||
|
function canonical(value: any): any {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.map(canonical).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
||||||
|
if (value && typeof value === 'object')
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.map(key => [key, canonical(value[key])]),
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [type, path, parameters] of calculations) {
|
||||||
|
test(`GET ${path} matches legacy ${type} results and access`, async ({
|
||||||
|
admin,
|
||||||
|
share,
|
||||||
|
viewer,
|
||||||
|
seed,
|
||||||
|
}) => {
|
||||||
|
const range = { ...dateRange(seed), timezone: 'UTC', unit: 'day' };
|
||||||
|
const body = {
|
||||||
|
websiteId: seed.website.id,
|
||||||
|
type,
|
||||||
|
filters: range,
|
||||||
|
parameters: {
|
||||||
|
startDate: new Date(range.startAt).toISOString(),
|
||||||
|
endDate: new Date(range.endAt).toISOString(),
|
||||||
|
timezone: 'UTC',
|
||||||
|
unit: 'day',
|
||||||
|
...parameters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const url = `/api/websites/${seed.website.id}/${path}`;
|
||||||
|
const params = serializeAnalyticsQuery({ ...range, ...parameters });
|
||||||
|
const legacy = await admin.post(`/api/reports/${type}`, body);
|
||||||
|
const result = await admin.get(url, { params });
|
||||||
|
expect(legacy.status).toBe(200);
|
||||||
|
expect(result.status).toBe(200);
|
||||||
|
expect(canonical(result.body)).toEqual(canonical(legacy.body));
|
||||||
|
expect((await viewer.get(url, { params })).status).toBe(401);
|
||||||
|
expect((await (await share()).get(url, { params })).status).toBe(
|
||||||
|
type === 'heatmap' ? 401 : 200,
|
||||||
|
);
|
||||||
|
expect((await admin.get(url)).status).toBe(400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [type, parameters] of [
|
||||||
|
[
|
||||||
|
'funnel',
|
||||||
|
{
|
||||||
|
window: 60,
|
||||||
|
steps: [
|
||||||
|
{ type: 'path', value: '/' },
|
||||||
|
{ type: 'event', value: 'signup' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['goal', { type: 'path', value: '/' }],
|
||||||
|
] as const) {
|
||||||
|
test(`${type} definitions preserve IDs and scope saved stats to the website`, async ({
|
||||||
|
admin,
|
||||||
|
viewer,
|
||||||
|
share,
|
||||||
|
seed,
|
||||||
|
}) => {
|
||||||
|
const base = `/api/websites/${seed.website.id}/${type}s`;
|
||||||
|
const created = await admin.post(base, { name: uniqueName(type), parameters });
|
||||||
|
expect(created.status).toBe(200);
|
||||||
|
const id = created.body.id;
|
||||||
|
try {
|
||||||
|
expect((await admin.get(`/api/reports/${id}`)).body.id).toBe(id);
|
||||||
|
expect((await admin.get(`${base}/${id}`)).body.parameters).toEqual(parameters);
|
||||||
|
expect((await (await share()).get(base)).status).toBe(200);
|
||||||
|
const range = dateRange(seed);
|
||||||
|
const saved = await admin.get(`${base}/${id}/stats`, { params: range });
|
||||||
|
const preview = await admin.get(`${base}/stats`, {
|
||||||
|
params: serializeAnalyticsQuery({ ...range, ...parameters }),
|
||||||
|
});
|
||||||
|
expect(saved.status).toBe(200);
|
||||||
|
expect(saved.body).toEqual(preview.body);
|
||||||
|
expect((await viewer.post(`${base}/${id}`, { name: 'Denied', parameters })).status).toBe(401);
|
||||||
|
const otherBase = `/api/websites/${seed.website2.id}/${type}s/${id}`;
|
||||||
|
expect((await admin.get(otherBase)).status).toBe(404);
|
||||||
|
expect((await admin.post(otherBase, { name: 'Wrong website', parameters })).status).toBe(404);
|
||||||
|
expect((await admin.del(otherBase)).status).toBe(404);
|
||||||
|
const otherType = type === 'goal' ? 'funnels' : 'goals';
|
||||||
|
expect((await admin.get(`/api/websites/${seed.website.id}/${otherType}/${id}`)).status).toBe(
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
const updated = await admin.post(`${base}/${id}`, {
|
||||||
|
name: 'Updated',
|
||||||
|
websiteId: seed.website2.id,
|
||||||
|
type: otherType,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
expect(updated.status).toBe(200);
|
||||||
|
expect(updated.body).toMatchObject({ id, name: 'Updated', websiteId: seed.website.id, type });
|
||||||
|
} finally {
|
||||||
|
expect((await admin.del(`${base}/${id}`)).status).toBe(200);
|
||||||
|
}
|
||||||
|
expect((await admin.get(`${base}/${id}`)).status).toBe(404);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('performance and UTM datasets reproduce legacy aggregates', async ({
|
||||||
|
admin,
|
||||||
|
viewer,
|
||||||
|
share,
|
||||||
|
seed,
|
||||||
|
}) => {
|
||||||
|
const range = { ...dateRange(seed), timezone: 'UTC', unit: 'day' };
|
||||||
|
const body = (type: string) => ({
|
||||||
|
websiteId: seed.website.id,
|
||||||
|
type,
|
||||||
|
filters: range,
|
||||||
|
parameters: {
|
||||||
|
startDate: new Date(range.startAt).toISOString(),
|
||||||
|
endDate: new Date(range.endAt).toISOString(),
|
||||||
|
timezone: 'UTC',
|
||||||
|
unit: 'day',
|
||||||
|
metric: 'lcp',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const base = `/api/websites/${seed.website.id}`;
|
||||||
|
const performance = await admin.post('/api/reports/performance', body('performance'));
|
||||||
|
expect(performance.status).toBe(200);
|
||||||
|
for (const [path, key] of [
|
||||||
|
['performance/stats', 'summary'],
|
||||||
|
['performance/chart', 'chart'],
|
||||||
|
] as const) {
|
||||||
|
const response = await admin.get(`${base}/${path}`, { params: range });
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(canonical(key === 'chart' ? response.body.chart : response.body)).toEqual(
|
||||||
|
canonical(performance.body[key]),
|
||||||
|
);
|
||||||
|
expect((await viewer.get(`${base}/${path}`, { params: range })).status).toBe(401);
|
||||||
|
expect((await (await share()).get(`${base}/${path}`, { params: range })).status).toBe(200);
|
||||||
|
}
|
||||||
|
for (const [type, key] of [
|
||||||
|
['path', 'pages'],
|
||||||
|
['title', 'pageTitles'],
|
||||||
|
['device', 'devices'],
|
||||||
|
['browser', 'browsers'],
|
||||||
|
]) {
|
||||||
|
const response = await admin.get(`${base}/performance/metrics`, { params: { ...range, type } });
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(canonical(response.body)).toEqual(canonical(performance.body[key]));
|
||||||
|
}
|
||||||
|
const utm = await admin.post('/api/reports/utm', body('utm'));
|
||||||
|
expect(utm.status).toBe(200);
|
||||||
|
for (const type of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']) {
|
||||||
|
const response = await admin.get(`${base}/utm/metrics`, { params: { ...range, type } });
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(canonical(response.body)).toEqual(canonical(utm.body[type]));
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
(await admin.get(`${base}/utm/metrics`, { params: { ...range, type: 'invalid' } })).status,
|
||||||
|
).toBe(400);
|
||||||
|
expect(
|
||||||
|
(await admin.get(`${base}/performance/metrics`, { params: { ...range, type: 'invalid' } }))
|
||||||
|
.status,
|
||||||
|
).toBe(400);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user