diff --git a/docs/feedback-voting.md b/docs/feedback-voting.md index 5debf205519..13d86d34fee 100644 --- a/docs/feedback-voting.md +++ b/docs/feedback-voting.md @@ -181,7 +181,7 @@ Votes you choose to share are sent to the Telemetry Backend immediately from the - App server responsibility: build the bundle, POST it to Telemetry Backend, update trace status - Telemetry Backend responsibility: authenticate the request, validate payload shape, compress/store the bundle, return the final object key - Retry behavior: failed uploads move to `failed` with an error message in `failureReason`, and the worker retries them on later ticks -- Configuration requirement: remote export only works when `PAPERCLIP_FEEDBACK_EXPORT_BACKEND_URL` or `PAPERCLIP_TELEMETRY_BACKEND_URL` is configured on the app server +- Default endpoint: when no feedback export backend URL is configured, Paperclip falls back to `https://telemetry.paperclip.ing` Exported objects use a deterministic key pattern so they are easy to inspect: diff --git a/server/src/__tests__/feedback-share-client.test.ts b/server/src/__tests__/feedback-share-client.test.ts new file mode 100644 index 00000000000..902ef1acdca --- /dev/null +++ b/server/src/__tests__/feedback-share-client.test.ts @@ -0,0 +1,48 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createFeedbackTraceShareClientFromConfig } from "../services/feedback-share-client.js"; + +describe("feedback trace share client", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ objectKey: "feedback-traces/test.json" }), + })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("defaults to telemetry.paperclip.ing when no backend url is configured", async () => { + const client = createFeedbackTraceShareClientFromConfig({ + feedbackExportBackendUrl: undefined, + feedbackExportBackendToken: undefined, + }); + + await client.uploadTraceBundle({ + traceId: "trace-1", + exportId: "export-1", + companyId: "company-1", + issueId: "issue-1", + issueIdentifier: "PAP-1", + adapterType: "codex_local", + captureStatus: "full", + notes: [], + envelope: {}, + surface: null, + paperclipRun: null, + rawAdapterTrace: null, + normalizedAdapterTrace: null, + privacy: null, + integrity: {}, + files: [], + }); + + expect(fetch).toHaveBeenCalledWith( + "https://telemetry.paperclip.ing/feedback-traces", + expect.objectContaining({ + method: "POST", + }), + ); + }); +}); diff --git a/server/src/services/feedback-share-client.ts b/server/src/services/feedback-share-client.ts index e1761b14720..e8d611bb536 100644 --- a/server/src/services/feedback-share-client.ts +++ b/server/src/services/feedback-share-client.ts @@ -1,6 +1,8 @@ import type { FeedbackTraceBundle } from "@paperclipai/shared"; import type { Config } from "../config.js"; +const DEFAULT_FEEDBACK_EXPORT_BACKEND_URL = "https://telemetry.paperclip.ing"; + function buildFeedbackShareObjectKey(bundle: FeedbackTraceBundle, exportedAt: Date) { const year = String(exportedAt.getUTCFullYear()); const month = String(exportedAt.getUTCMonth() + 1).padStart(2, "0"); @@ -14,10 +16,8 @@ export interface FeedbackTraceShareClient { export function createFeedbackTraceShareClientFromConfig( config: Pick, -): FeedbackTraceShareClient | null { - const baseUrl = config.feedbackExportBackendUrl?.trim(); - if (!baseUrl) return null; - +): FeedbackTraceShareClient { + const baseUrl = config.feedbackExportBackendUrl?.trim() || DEFAULT_FEEDBACK_EXPORT_BACKEND_URL; const token = config.feedbackExportBackendToken?.trim(); const endpoint = new URL("/feedback-traces", baseUrl).toString();