-
Notifications
You must be signed in to change notification settings - Fork 372
feat: add Webvizio integration #1045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { sharepoint } from '@corsair-dev/sharepoint'; | |
| import { slack } from '@corsair-dev/slack'; | ||
| import { twilio } from '@corsair-dev/twilio'; | ||
| import { vapi } from '@corsair-dev/vapi'; | ||
| import { webvizio } from '@corsair-dev/webvizio'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 2 '"`@corsair-dev/webvizio`"' --glob 'package.json' .Repository: corsairdev/corsair Length of output: 403 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate manifests ---'
find . -name package.json -print | sort
printf '%s\n' '--- demo package manifest ---'
demo_manifest="$(find demo/testing -name package.json -print -quit)"
if [ -n "${demo_manifest}" ]; then
cat -n "${demo_manifest}"
else
printf '%s\n' 'No package.json found under demo/testing'
fi
printf '%s\n' '--- workspace configuration ---'
rg -n -C 3 'workspaces|demo/testing|`@corsair-dev/webvizio`' package.json pnpm-workspace.yaml yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null || true
printf '%s\n' '--- relevant source and package metadata ---'
cat -n demo/testing/src/server/corsair.ts | sed -n '1,100p'
cat -n packages/webvizio/package.jsonRepository: corsairdev/corsair Length of output: 14227 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- root workspace manifest ---'
cat -n package.json | sed -n '1,100p'
printf '%s\n' '--- lockfile entry for demo/testing ---'
sed -n '160,245p' pnpm-lock.yaml
printf '%s\n' '--- all Webvizio references ---'
rg -n -C 2 '`@corsair-dev/webvizio`|webvizio' demo packages package.json pnpm-lock.yaml pnpm-workspace.yaml 2>/dev/null || true
printf '%s\n' '--- deterministic dependency check ---'
python3 - <<'PY'
import json
from pathlib import Path
manifest = json.loads(Path("demo/testing/package.json").read_text())
source = Path("demo/testing/src/server/corsair.ts").read_text()
name = "`@corsair-dev/webvizio`"
declared = name in manifest.get("dependencies", {}) or name in manifest.get("devDependencies", {})
print(f"source_import_present={f\"from '{name}'\" in source}")
print(f"demo_testing_declares_dependency={declared}")
print(f"root_declares_dependency={name in json.loads(Path('package.json').read_text()).get('dependencies', {})}")
PYRepository: corsairdev/corsair Length of output: 14551 Declare The demo imports this package, but neither its manifest nor lockfile declares it. Add 🤖 Prompt for AI Agents |
||
| import { createCorsair } from 'corsair'; | ||
|
|
||
| import { sqlite } from '../db'; | ||
|
|
@@ -63,6 +64,9 @@ export const corsair = createCorsair({ | |
| key: process.env.VAPI_API_KEY, | ||
| webhookSecret: process.env.VAPI_WEBHOOK_SECRET, | ||
| }), | ||
| webvizio({ | ||
| key: process.env.WEBVIZIO_API_KEY, | ||
| }), | ||
| instagram(), | ||
| ], | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; | ||
| import { ApiError, request } from 'corsair/http'; | ||
|
|
||
| export class WebvizioAPIError extends Error { | ||
| constructor( | ||
| message: string, | ||
| public readonly code?: string | number, | ||
| ) { | ||
| super(message); | ||
| this.name = 'WebvizioAPIError'; | ||
| } | ||
| } | ||
|
|
||
| const WEBVIZIO_MCP_API_BASE = 'https://app.webvizio.com/api/mcp/v1'; | ||
| const WEBVIZIO_WEBHOOK_API_BASE = 'https://app.webvizio.com/api/v1'; | ||
|
|
||
| export async function makeWebvizioRequest<T>( | ||
| endpoint: string, | ||
| apiKey: string, | ||
| options: { | ||
| method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; | ||
| body?: Record<string, unknown>; | ||
| query?: Record<string, string | number | boolean | undefined>; | ||
| baseUrl?: string; | ||
| } = {}, | ||
| ): Promise<T> { | ||
| const { | ||
| method = 'GET', | ||
| body, | ||
| query, | ||
| baseUrl = WEBVIZIO_MCP_API_BASE, | ||
| } = options; | ||
|
|
||
| const config: OpenAPIConfig = { | ||
| BASE: baseUrl, | ||
| VERSION: '1.0.0', | ||
| WITH_CREDENTIALS: false, | ||
| CREDENTIALS: 'omit', | ||
| TOKEN: apiKey, | ||
| HEADERS: { | ||
| 'Content-Type': 'application/json', | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${apiKey}`, | ||
| }, | ||
| }; | ||
|
|
||
| const requestOptions: ApiRequestOptions = { | ||
| method, | ||
| url: endpoint, | ||
| body: | ||
| method === 'POST' || method === 'PUT' || method === 'PATCH' | ||
| ? body | ||
| : undefined, | ||
| mediaType: 'application/json', | ||
| query, | ||
| }; | ||
|
|
||
| try { | ||
| return await request<T>(config, requestOptions); | ||
| } catch (error) { | ||
| if (error instanceof ApiError) { | ||
| const detail = | ||
| typeof error.body === 'object' | ||
| ? JSON.stringify(error.body) | ||
| : String(error.body ?? ''); | ||
|
|
||
| throw new WebvizioAPIError( | ||
| `${error.message} (status=${error.status}, body=${detail})`, | ||
| error.status, | ||
| ); | ||
|
Comment on lines
+61
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files 'packages/webvizio/*' 'packages/corsair/*' | sort
printf '%s\n' '--- client.ts ---'
cat -n packages/webvizio/client.ts | sed -n '1,130p'
printf '%s\n' '--- error-handlers.ts ---'
cat -n packages/webvizio/error-handlers.ts | sed -n '1,220p'
printf '%s\n' '--- ApiError and WebvizioAPIError definitions/usages ---'
rg -n -C 5 'class ApiError|interface ApiError|type ApiError|retryAfter|class WebvizioAPIError|new WebvizioAPIError|instanceof WebvizioAPIError|errorHandlers|errorHandlers' packagesRepository: corsairdev/corsair Length of output: 50375 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Webvizio files ---'
git ls-files | rg '(^|/)webvizio(/|$)' | sort
printf '%s\n' '--- Webvizio client ---'
cat -n packages/webvizio/client.ts | sed -n '1,120p'
printf '%s\n' '--- Webvizio error handlers ---'
cat -n packages/webvizio/error-handlers.ts | sed -n '1,160p'
printf '%s\n' '--- Webvizio tests ---'
git ls-files packages/webvizio | rg '(\.test\.|\.spec\.)' | while read -r f; do
echo "--- $f"
rg -n -C 4 'retryAfter|429|ApiError|WebvizioAPIError|RATE_LIMIT' "$f" || true
done
printf '%s\n' '--- Corsair ApiError definition and request catch path ---'
rg -l 'class ApiError|retryAfter' packages/corsair | sort | while read -r f; do
echo "--- $f"
rg -n -C 6 'class ApiError|retryAfter|throw new ApiError|new ApiError' "$f"
doneRepository: corsairdev/corsair Length of output: 18916 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- request defaults and final ApiError propagation ---'
cat -n packages/corsair/async-core/request.ts | sed -n '329,455p'
printf '%s\n' '--- Webvizio registration ---'
rg -n -C 8 'errorHandlers|makeWebvizioRequest|WebvizioAPIError' packages/webvizio/index.ts packages/webvizio/endpoints
printf '%s\n' '--- error dispatch and retry delay use ---'
rg -n -C 10 'handleCorsairError|headersRetryAfterMs|originalError|errorHandlers' packages/corsair/core packages/corsair/async-core
printf '%s\n' '--- analogous API error conversions ---'
for f in packages/facebook/client.ts packages/googlemaps/client.ts packages/vercel/client.ts packages/youtube/client.ts packages/witai/client.ts; do
if [ -f "$f" ]; then
echo "--- $f"
rg -n -C 8 'class .*APIError|if \(error instanceof ApiError\)|retryAfter|status' "$f"
fi
doneRepository: corsairdev/corsair Length of output: 42385 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
client = Path("packages/webvizio/client.ts").read_text()
handlers = Path("packages/webvizio/error-handlers.ts").read_text()
request = Path("packages/corsair/async-core/request.ts").read_text()
bind = Path("packages/corsair/core/endpoints/bind.ts").read_text()
assert "public readonly code?: string | number" in client
assert "public readonly status" not in client
assert "public readonly retryAfter" not in client
assert re.search(r"new WebvizioAPIError\([^;]*error\.status", client, re.S)
assert "error.retryAfter" not in client
assert "error instanceof ApiError && error.retryAfter !== undefined" in handlers
assert "headersRetryAfterMs: retryAfterMs" in handlers
assert "DEFAULT_RATE_LIMIT_CONFIG" in request
assert "rateLimitConfig?.rateLimitConfig" not in request
assert "headersRetryAfterMs" in bind
print("WebvizioAPIError exposes code only; it does not expose status or retryAfter.")
print("makeWebvizioRequest passes status as code and does not pass retryAfter.")
print("Webvizio RATE_LIMIT_ERROR reads retryAfter only from ApiError.")
print("The endpoint retry loop uses headersRetryAfterMs when it is provided.")
PYRepository: corsairdev/corsair Length of output: 438 🏁 Script executed: #!/bin/bash
set -eu
cat -n packages/corsair/core/endpoints/bind.ts | sed -n '204,245p'Repository: corsairdev/corsair Length of output: 1720 Preserve 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| if (error instanceof WebvizioAPIError) { | ||
| throw error; | ||
| } | ||
|
|
||
| throw new WebvizioAPIError( | ||
| error instanceof Error ? error.message : 'Unknown error', | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { list as projectsList } from './projects'; | ||
| import { list as webhooksList } from './webhooks'; | ||
|
|
||
| export const Projects = { | ||
| list: projectsList, | ||
| }; | ||
|
|
||
| export const Webhooks = { | ||
| list: webhooksList, | ||
| }; | ||
|
|
||
| export * from './types'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { logEventFromContext } from 'corsair/core'; | ||
| import { makeWebvizioRequest } from '../client'; | ||
| import type { WebvizioEndpoints } from '../index'; | ||
|
|
||
| export const list: WebvizioEndpoints['projectsList'] = async (ctx, input) => { | ||
| const result = await makeWebvizioRequest<unknown[]>( | ||
| '/projects', | ||
| ctx.key, | ||
| ); | ||
|
|
||
| await logEventFromContext( | ||
| ctx, | ||
| 'webvizio.projects.list', | ||
| { ...input }, | ||
| 'completed', | ||
| ); | ||
|
|
||
| return result as WebvizioEndpoints['projectsList'] extends ( | ||
| ctx: infer _, | ||
| input: infer _, | ||
| ) => Promise<infer R> | ||
| ? R | ||
| : never; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| const WebvizioProjectSchema = z | ||
| .object({ | ||
| id: z.string(), | ||
| name: z.string().optional(), | ||
| description: z.string().optional(), | ||
| }) | ||
| .passthrough(); | ||
|
|
||
| export type WebvizioProject = z.infer<typeof WebvizioProjectSchema>; | ||
|
|
||
| const WebvizioWebhookSubscriptionSchema = z | ||
| .object({ | ||
| id: z.string().optional(), | ||
| url: z.string().optional(), | ||
| event: z.string().optional(), | ||
| }) | ||
| .passthrough(); | ||
|
|
||
| export type WebvizioWebhookSubscription = z.infer< | ||
| typeof WebvizioWebhookSubscriptionSchema | ||
| >; | ||
|
|
||
| export type WebvizioEndpointInputs = { | ||
| projectsList: Record<string, never>; | ||
| webhooksList: Record<string, never>; | ||
| }; | ||
|
|
||
| export type WebvizioEndpointOutputs = { | ||
| projectsList: WebvizioProject[]; | ||
| webhooksList: WebvizioWebhookSubscription[]; | ||
| }; | ||
|
|
||
| export const WebvizioEndpointInputSchemas = { | ||
| projectsList: z.object({}), | ||
| webhooksList: z.object({}), | ||
| } as const; | ||
|
|
||
| export const WebvizioEndpointOutputSchemas = { | ||
| projectsList: z.array(WebvizioProjectSchema), | ||
| webhooksList: z.array(WebvizioWebhookSubscriptionSchema), | ||
| } as const; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { logEventFromContext } from 'corsair/core'; | ||
| import { makeWebvizioRequest } from '../client'; | ||
| import type { WebvizioEndpoints } from '../index'; | ||
|
|
||
| export const list: WebvizioEndpoints['webhooksList'] = async (ctx, input) => { | ||
| const result = await makeWebvizioRequest<unknown[]>( | ||
| '/webhook', | ||
| ctx.key, | ||
| { | ||
| baseUrl: 'https://app.webvizio.com/api/v1', | ||
| }, | ||
| ); | ||
|
|
||
| await logEventFromContext( | ||
| ctx, | ||
| 'webvizio.webhooks.list', | ||
| { ...input }, | ||
| 'completed', | ||
| ); | ||
|
|
||
| return result as WebvizioEndpoints['webhooksList'] extends ( | ||
| ctx: infer _, | ||
| input: infer _, | ||
| ) => Promise<infer R> | ||
| ? R | ||
|
|
||
| : never; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { ApiError } from 'corsair/http'; | ||
| import type { CorsairErrorHandler } from 'corsair/core'; | ||
|
|
||
| export const errorHandlers = { | ||
| RATE_LIMIT_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 429) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('rate_limited') || msg.includes('429'); | ||
| }, | ||
| handler: async (error: Error) => { | ||
| let retryAfterMs: number | undefined; | ||
| if (error instanceof ApiError && error.retryAfter !== undefined) { | ||
| retryAfterMs = error.retryAfter; | ||
| } | ||
| return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; | ||
| }, | ||
| }, | ||
| AUTH_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 401) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('unauthorized') || msg.includes('invalid_auth'); | ||
| }, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| DEFAULT: { | ||
| match: () => true, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| } satisfies CorsairErrorHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 20594
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 50375
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 18962
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 6374
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 50377
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 22480
Guard the Webvizio call when no API key is available.
WEBVIZIO_API_KEYis optional. Without an explicit or stored key, the Webvizio key builder returns'', andprojects.listsendsAuthorization: Bearer. Require the key before starting the script, or skip the Webvizio call when no key is configured.🤖 Prompt for AI Agents