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"importtype { IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, ServerResponse,} from"node:http"typeHandler= ( 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 },) =>voidconstnormalizeHeaders= ( headers:IncomingHttpHeaders|OutgoingHttpHeaders,) => {constentries=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][], )returnnewHeaders(entries)}constnormalizeRequest= (req:IncomingMessage& { body:any }) => {constprotocol=req.headers["x-forwarded-proto"] ||"http"consthost=req.headers["x-forwarded-host"] ||req.headers.host ||"unknown"returnnewRequest(newURL(req.url!,`${protocol}://${host}`), { method:req.method, headers:normalizeHeaders(req.headers), body:req.body ||null, })}constnormalizeResponse= ( res:ServerResponse, body:object|string|Buffer|null|undefined,) => {constresponseHeaders=normalizeHeaders(res.getHeaders())// 204 No Content, 304 Not Modified don't allow body https://nextjs.org/docs/messages/invalid-api-status-bodyif (res.statusCode ===204||res.statusCode ===304) { body =null }// Response accepts only string or Buffer and next supports objectsif (body &&typeof body ==="object"&&!Buffer.isBuffer(body)) { body =JSON.stringify(body) }returnnewResponse(body, { status:res.statusCode, statusText:res.statusMessage, headers: responseHeaders, })}exportfunctionwithAppear(handler:Handler, config:AppearConfig):Handler {returnasync (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|undefinedconstres=newProxy(baseRes, {get(target, prop, receiver) {if (prop ==="json"|| prop ==="send") {return (content:any) => { body = contentreturnReflect.get(target, prop, receiver)(content) } }returnReflect.get(target, prop, receiver) }, })constresult=awaithandler(req, res)try {constrequest=normalizeRequest(req)constresponse=normalizeResponse(res, body)constoperation=awaitprocess({ request, response, direction:"incoming", })// report, don't await so we don't slow down response timewaitUntil(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.