ソースを参照

Carry the vendor's request id on connector errors, so Intuit's intuit_tid reaches the job log (#332)

Bernt Christian Egeland 3 週間 前
親
コミット
c9fae5bfa0

+ 19 - 2
src/__tests__/features/integrations/quickbooks-server.test.ts

@@ -55,11 +55,17 @@ interface Call {
 
 type Answer = (call: Call) => unknown
 
-function fault(status: number, code: string, message: string): ConnectorHttpError {
+function fault(
+  status: number,
+  code: string,
+  message: string,
+  requestId: string | null = null
+): ConnectorHttpError {
   return new ConnectorHttpError(
     status,
     JSON.stringify({ Fault: { Error: [{ Message: message, Detail: message, code }] } }),
-    'https://quickbooks.api.intuit.com/x'
+    'https://quickbooks.api.intuit.com/x',
+    requestId
   )
 }
 
@@ -1073,6 +1079,17 @@ describe('QuickBooks: connecting', () => {
     expect(t.logs.some((l) => l.message.includes('Sales tax is switched off'))).toBe(true)
   })
 
+  it('carries Intuit’s request id on a failure, for their support', async () => {
+    const t = makeCtx({
+      answer: () => {
+        throw fault(500, '10000', 'Internal error', 'tid-abc-123')
+      },
+    })
+    const res = await connector.test(t.ctx)
+    expect(res.ok).toBe(false)
+    expect(res.message).toContain('intuit_tid tid-abc-123')
+  })
+
   it('reports a failed test with the vendor wording', async () => {
     const t = makeCtx({ answer: () => fault(401, '3200', 'AuthenticationFailed') })
     t.ctx.http.json = async () => {

+ 6 - 1
src/features/integrations/Lib/http.ts

@@ -32,6 +32,11 @@ async function sleep(ms: number): Promise<void> {
   await new Promise((resolve) => setTimeout(resolve, ms))
 }
 
+/** The vendor's request id, in the header Intuit and most others use for it. */
+function requestIdOf(res: Response): string | null {
+  return res.headers.get('intuit_tid') ?? res.headers.get('x-request-id') ?? null
+}
+
 function retryDelayMs(attempt: number, res: Response | null): number {
   const retryAfter = res?.headers.get('retry-after')
   if (retryAfter) {
@@ -143,7 +148,7 @@ export function createConnectorHttp(input: {
     async json<T>(url: string, init?: RequestInit): Promise<T> {
       const res = await doFetch(url, init)
       const text = await res.text()
-      if (!res.ok) throw new ConnectorHttpError(res.status, text, url)
+      if (!res.ok) throw new ConnectorHttpError(res.status, text, url, requestIdOf(res))
       if (!text) return undefined as T
       return JSON.parse(text) as T
     },

+ 11 - 2
src/features/integrations/Lib/types.ts

@@ -134,10 +134,19 @@ export interface ConnectorHttp {
 export class ConnectorHttpError extends Error {
   status: number
   body: string
-  constructor(status: number, body: string, url: string) {
-    super(`HTTP ${status} from ${url}: ${body.slice(0, 300)}`)
+  /**
+   * The vendor's id for the request (Intuit's intuit_tid, or x-request-id),
+   * which is what their support asks for first. Kept on the error so it
+   * reaches the job log.
+   */
+  requestId: string | null
+  constructor(status: number, body: string, url: string, requestId: string | null = null) {
+    super(
+      `HTTP ${status} from ${url}: ${body.slice(0, 300)}${requestId ? ` (request ${requestId})` : ''}`
+    )
     this.status = status
     this.body = body
+    this.requestId = requestId
   }
 }
 

+ 6 - 3
src/integrations/quickbooks/server.ts

@@ -76,10 +76,13 @@ const TOTAL_TOLERANCE = 0.05
 class QboError extends Error {
   status: number
   code: string | null
-  constructor(status: number, body: string) {
-    super(`QuickBooks: ${faultMessage(body)}`)
+  /** Intuit's intuit_tid for the failed call; their support asks for it. */
+  requestId: string | null
+  constructor(status: number, body: string, requestId: string | null = null) {
+    super(`QuickBooks: ${faultMessage(body)}${requestId ? ` (intuit_tid ${requestId})` : ''}`)
     this.status = status
     this.code = faultCode(body)
+    this.requestId = requestId
   }
 }
 
@@ -181,7 +184,7 @@ async function api<T>(
       ...(init.body !== undefined && { body: JSON.stringify(init.body) }),
     })
   } catch (err) {
-    if (err instanceof ConnectorHttpError) throw new QboError(err.status, err.body)
+    if (err instanceof ConnectorHttpError) throw new QboError(err.status, err.body, err.requestId)
     throw err
   }
 }