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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function formatTrackedRunKind(kind: RunKind): string {
case RunKinds['gsc-sync']: return 'GSC sync'
case RunKinds['inspect-sitemap']: return 'Sitemap inspection'
case RunKinds['ga-sync']: return 'GA sync'
case RunKinds['traffic-sync']: return 'Traffic sync'
case RunKinds['bing-inspect']: return 'Bing URL inspection'
case RunKinds['bing-inspect-sitemap']: return 'Bing sitemap inspection'
case RunKinds['site-audit']: return 'Site audit'
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/build-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function kindLabel(kind: RunKind): string {
case RunKinds['gsc-sync']: return 'GSC sync'
case RunKinds['inspect-sitemap']: return 'Sitemap inspection'
case RunKinds['ga-sync']: return 'GA sync'
case RunKinds['traffic-sync']: return 'Traffic sync'
case RunKinds['bing-inspect']: return 'Bing URL inspection'
case RunKinds['bing-inspect-sitemap']: return 'Bing sitemap inspection'
case RunKinds['site-audit']: return 'Site audit'
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/queries/run-invalidations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function invalidateQueriesForRunKind(
void queryClient.invalidateQueries({ queryKey: queryKeys.gsc.project(projectName) })
return
case RunKinds['ga-sync']:
case RunKinds['traffic-sync']:
void queryClient.invalidateQueries({ queryKey: queryKeys.traffic.project(projectName) })
return
case RunKinds['bing-inspect']:
Expand Down
14 changes: 14 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ erDiagram
projects ||--o{ bing_keyword_stats : has
projects ||--o{ bing_coverage_snapshots : has

projects ||--o{ traffic_sources : has
traffic_sources ||--o{ crawler_events_hourly : "rolls up"
traffic_sources ||--o{ ai_referral_events_hourly : "rolls up"
traffic_sources ||--o{ raw_event_samples : "samples"

projects ||--o| agent_sessions : "has (1:1)"
projects ||--o{ agent_memory : has
```
Expand Down Expand Up @@ -81,6 +86,15 @@ erDiagram
| **ga_ai_referrals** | AI engine referral tracking. Unique: `(projectId, date, source, medium, sourceDimension)` |
| **ga_social_referrals** | Social media referral tracking. Unique: `(projectId, date, source, medium, channelGroup)` |

### Server-Side Traffic Ingestion

| Table | Purpose |
|-------|---------|
| **traffic_sources** | Per-connection metadata (Cloud Run today; future WordPress / Cloudflare / Vercel). Status `connected` / `paused` / `error` / `archived`. Credentials live in `~/.canonry/config.yaml`, never here. FK: projectId → projects. |
| **crawler_events_hourly** | Hourly rollup of server-observed AI crawler hits. Composite PK `(projectId, sourceId, tsHour, botId, verificationStatus, pathNormalized, status)` so repeat syncs upsert via `hits + ?`. |
| **ai_referral_events_hourly** | Hourly rollup of server-observed human AI-referral clicks (UTM or referer evidence). Composite PK matches the crawler bucket pattern. |
| **raw_event_samples** | Bounded sample tail for classifier debugging (default 30-day retention). FK: sourceId → traffic_sources. |

### Intelligence

| Table | Purpose |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canonry",
"private": true,
"version": "4.11.1",
"version": "4.12.1",
"type": "module",
"packageManager": "[email protected]",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/api-routes/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Shared Fastify route plugins used by both the local server (`packages/canonry`)
| `src/report.ts` | `GET /projects/:name/report` (JSON DTO) and `GET /projects/:name/report.html` (standalone downloadable HTML) — aggregated client-facing AEO report bundle (13 sections) |
| `src/report-renderer.ts` | `renderReportHtml(report)` — server-side HTML renderer with inline SVG charts and inline CSS, re-exported from `@ainyc/canonry-api-routes` for the CLI |
| `src/wordpress.ts` | WordPress integration routes |
| `src/traffic.ts` | Server-side traffic ingestion routes — `POST /traffic/connect/cloud-run`, `POST /traffic/sources/:id/sync`. Credentials resolved through an injected `cloudRunCredentialStore`; the Cloud Logging pull and access-token resolver are also injectable for tests. |
| `src/backlinks.ts` | Backlinks (Common Crawl sync + per-project extract/summary/domains/history) routes |
| `src/doctor.ts` | `GET /doctor` and `GET /projects/:name/doctor` — runs check registry, returns `DoctorReport` |
| `src/doctor/registry.ts` | `ALL_CHECKS` — single source of truth for the doctor check catalog |
Expand Down
2 changes: 2 additions & 0 deletions packages/api-routes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"@ainyc/canonry-db": "workspace:*",
"@ainyc/canonry-intelligence": "workspace:*",
"@ainyc/canonry-integration-bing": "workspace:*",
"@ainyc/canonry-integration-cloud-run": "workspace:*",
"@ainyc/canonry-integration-google": "workspace:*",
"@ainyc/canonry-integration-commoncrawl": "workspace:*",
"@ainyc/canonry-integration-google-analytics": "workspace:*",
"@ainyc/canonry-integration-traffic": "workspace:*",
"@ainyc/canonry-integration-wordpress": "workspace:*",
"drizzle-orm": "^0.45.1",
"fastify": "^5.4.0"
Expand Down
13 changes: 13 additions & 0 deletions packages/api-routes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { wordpressRoutes } from './wordpress.js'
import type { WordpressRoutesOptions } from './wordpress.js'
import { backlinksRoutes } from './backlinks.js'
import type { BacklinksRoutesOptions } from './backlinks.js'
import { trafficRoutes } from './traffic.js'
import type { TrafficRoutesOptions, CloudRunCredentialStore } from './traffic.js'
import { doctorRoutes } from './doctor.js'

declare module 'fastify' {
Expand Down Expand Up @@ -105,6 +107,12 @@ export interface ApiRoutesOptions {
onCdpConfigure?: CDPRoutesOptions['onCdpConfigure']
/** GA4 credential store — stores service account keys in config, not DB */
ga4CredentialStore?: Ga4CredentialStore
/** Cloud Run credential store — stores SA keys / OAuth tokens in config, not DB */
cloudRunCredentialStore?: CloudRunCredentialStore
/** Override Cloud Run pull (tests) — see `TrafficRoutesOptions` */
pullCloudRunEvents?: TrafficRoutesOptions['pullCloudRunEvents']
/** Override Cloud Run access-token resolver (tests) — see `TrafficRoutesOptions` */
resolveCloudRunAccessToken?: TrafficRoutesOptions['resolveCloudRunAccessToken']
/** Backlinks feature callbacks — see `backlinksRoutes` for details. */
getBacklinksStatus?: BacklinksRoutesOptions['getBacklinksStatus']
onInstallBacklinks?: BacklinksRoutesOptions['onInstallBacklinks']
Expand Down Expand Up @@ -262,6 +270,11 @@ export async function apiRoutes(app: FastifyInstance, opts: ApiRoutesOptions) {
googleConnectionStore: opts.googleConnectionStore,
getGoogleAuthConfig: opts.getGoogleAuthConfig,
} satisfies GA4RoutesOptions)
await api.register(trafficRoutes, {
cloudRunCredentialStore: opts.cloudRunCredentialStore,
pullCloudRunEvents: opts.pullCloudRunEvents,
resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken,
} satisfies TrafficRoutesOptions)
// Always mount the backlinks routes so read endpoints (summary, domains,
// history, sync list) work off the shared DB. Action routes (install,
// sync, extract, cache prune) throw MISSING_DEPENDENCY when the host
Expand Down
62 changes: 62 additions & 0 deletions packages/api-routes/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,68 @@ const routeCatalog: OpenApiOperation[] = [
404: { description: 'Project not found.' },
},
},
{
method: 'post',
path: '/api/v1/projects/{name}/traffic/connect/cloud-run',
summary: 'Connect a Cloud Run traffic source',
description:
'Stores the service-account JSON in `~/.canonry/config.yaml` and creates a `traffic_sources` row for the project. Reconnecting updates the existing active source rather than creating a duplicate.',
tags: ['traffic'],
parameters: [nameParameter],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['gcpProjectId', 'keyJson'],
properties: {
gcpProjectId: stringSchema,
serviceName: stringSchema,
location: stringSchema,
displayName: stringSchema,
keyJson: { ...stringSchema, description: 'Service-account JSON content.' },
},
},
},
},
},
responses: {
200: { description: 'Traffic source DTO returned.' },
400: { description: 'Invalid Cloud Run connection request.' },
404: { description: 'Project not found.' },
},
},
{
method: 'post',
path: '/api/v1/projects/{name}/traffic/sources/{id}/sync',
summary: 'Trigger a sync run for a traffic source',
description:
'Pulls request logs from the configured Cloud Run service for the lookback window, classifies crawler / AI-referral hits, and upserts hourly buckets and a bounded sample tail.',
tags: ['traffic'],
parameters: [
nameParameter,
{ name: 'id', in: 'path', required: true, description: 'Traffic source ID.', schema: stringSchema },
],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
sinceMinutes: { ...integerSchema, description: 'Lookback window in minutes (default 60).' },
},
},
},
},
},
responses: {
200: { description: 'Sync summary returned.' },
400: { description: 'Invalid sync request, missing credentials, or upstream pull error.' },
404: { description: 'Project or traffic source not found.' },
},
},
]

/**
Expand Down
Loading
Loading