Appear docs
HomepageGitHubAPI toolkitSign inGet demo
  • Getting Started
    • Welcome to Appear
    • How Appear works
    • Who is Appear for?
    • Get In Touch
    • Installation
  • Installation
    • Javascript / Typescript
    • Framework specific installations
      • NestJS
      • NextJS
      • Custom integrations
  • Explanations
    • Branches and environments
  • Connections
  • Managing your APIs
    • Creating a service
    • Add service via URL
    • Grouping & filtering
    • Editing your APIs
    • Overriding/updating a service
  • OpenAPI spec version
  • Tagging services
  • Service Resources
    • Resource map
  • Managing your organisation
    • Managing team members
    • Adding verified domains
  • Resources
    • FAQs
    • Product Map
    • Open-source
Powered by GitBook
On this page

Was this helpful?

  1. Installation
  2. Framework specific installations

Custom integrations

PreviousNextJSNextBranches and environments

Last updated 1 month ago

Was this helpful?

For some frameworks or situations (e.g., edge environments), automatic instrumentation isn't possible. However, you can still manually wrap your handlers to instrument them.

In essence, there are three steps to implement custom integration:

1

Normalise your request and response

Get and normalize Request & Response objects - this heavily depends on your framework and runtime

2

Process the operation

Call await process({ request, response, direction }) to process the request and response into an operation

3

Report to Appear

Call report({ operations, config }) to report the processed operations to Appear

Example integration

In the below example:

  • We wrap an Express-style request handler to add Appear reporting.

  • Since Express-style handlers typically use res.json() or res.send() to set the response body, we use a Proxy to intercept and capture the response content.

  • We normalize the Request, Response, and Headers objects so they work seamlessly with Appear's process() function.

  • We use Vercel’s waitUntil() to ensure the report is sent before the serverless function finishes, without delaying the response to the user.

This example is intentionally verbose to highlight edge cases you may need to consider. In most cases, your integration will be much simpler.

View example
import { process, report, AppearConfig } from "@appear.sh/introspector"
import { waitUntil } from "@vercel/functions"
import type {
  IncomingHttpHeaders,
  IncomingMessage,
  OutgoingHttpHeaders,
  ServerResponse,
} from "node:http"

type Handler = (
  req: IncomingMessage & {
    query: Partial<{ [key: string]: string | string[] }>
    cookies: Partial<{ [key: string]: string }>
    body: any
    env: { [key: string]: string | undefined }
  },
  res: ServerResponse & {
    send: any
    json: any
    status: any
  },
) => void

const normalizeHeaders = (
  headers: IncomingHttpHeaders | OutgoingHttpHeaders,
) => {
  const entries = Object.entries(headers).reduce(
    (acc, [key, value]) => {
      if (typeof value === "string") acc.push([key, value])
      if (typeof value === "number") acc.push([key, value.toString()])
      if (Array.isArray(value)) value.forEach((v) => acc.push([key, v]))
      return acc
    },
    [] as [string, string][],
  )
  return new Headers(entries)
}

const normalizeRequest = (req: IncomingMessage & { body: any }) => {
  const protocol = req.headers["x-forwarded-proto"] || "http"
  const host = req.headers["x-forwarded-host"] || req.headers.host || "unknown"

  return new Request(new URL(req.url!, `${protocol}://${host}`), {
    method: req.method,
    headers: normalizeHeaders(req.headers),
    body: req.body || null,
  })
}

const normalizeResponse = (
  res: ServerResponse,
  body: object | string | Buffer | null | undefined,
) => {
  const responseHeaders = normalizeHeaders(res.getHeaders())
  // 204 No Content, 304 Not Modified don't allow body https://nextjs.org/docs/messages/invalid-api-status-body
  if (res.statusCode === 204 || res.statusCode === 304) {
    body = null
  }
  // Response accepts only string or Buffer and next supports objects
  if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
    body = JSON.stringify(body)
  }
  return new Response(body, {
    status: res.statusCode,
    statusText: res.statusMessage,
    headers: responseHeaders,
  })
}

export function withAppear(handler: Handler, config: AppearConfig): Handler {
  return async (req, baseRes) => {
    // create a proxy to capture the response body
    // we need to do this because the syntax is res.json({ some: content })
    let body: object | string | Buffer | null | undefined
    const res = new Proxy(baseRes, {
      get(target, prop, receiver) {
        if (prop === "json" || prop === "send") {
          return (content: any) => {
            body = content
            return Reflect.get(target, prop, receiver)(content)
          }
        }
        return Reflect.get(target, prop, receiver)
      },
    })

    const result = await handler(req, res)
    try {
      const request = normalizeRequest(req)
      const response = normalizeResponse(res, body)
      const operation = await process({
        request,
        response,
        direction: "incoming",
      })

      // report, don't await so we don't slow down response time
      waitUntil(report(operation, config))
    } catch (e) {
      console.error("[Appear introspector] failed with error", e)
    }
    return result
  }
}


If you have any queries or require support with the above instructions, please .

contact us