From 3be71ced5f9e0f22cf4a3b8b87bae05e7a5f52f1 Mon Sep 17 00:00:00 2001 From: Steve Dodier-Lazaro Date: Sun, 27 Oct 2024 14:02:49 +0100 Subject: [PATCH] feat(rest): Add unique user-agent to requests --- .../rest/src/__tests__/interceptors.spec.ts | 21 ++++++- packages/rest/src/client.ts | 9 ++- packages/rest/src/interceptors.ts | 58 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/rest/src/__tests__/interceptors.spec.ts b/packages/rest/src/__tests__/interceptors.spec.ts index addc56f..84bdd6a 100644 --- a/packages/rest/src/__tests__/interceptors.spec.ts +++ b/packages/rest/src/__tests__/interceptors.spec.ts @@ -7,7 +7,11 @@ import { vol } from 'memfs'; import * as rateLimitModule from '../rateLimit'; import { Cache, generatePredictableKey } from '../cache'; -import { cacheInvalidationRequestInterceptor, rateLimitRequestInterceptor } from '../interceptors'; +import { + cacheInvalidationRequestInterceptor, + rateLimitRequestInterceptor, + userAgentRequestInterceptor, +} from '../interceptors'; import { Basic200 } from '../__fixtures__/storage'; import { Client } from '../client'; import { fileRequest } from '../__fixtures__/fileRequest'; @@ -71,6 +75,21 @@ describe('@figmarine/rest - interceptors', () => { vi.restoreAllMocks(); }); + describe('userAgentRequestInterceptor', () => { + it('returns a function when called', () => { + const interceptor = userAgentRequestInterceptor(); + expect(typeof interceptor).toBe('function'); + }); + + it('adds a User-Agent header containing the client and runtime names', async () => { + const interceptor = userAgentRequestInterceptor(); + + expect(fileRequest.headers['User-Agent']).toMatch(/^axios\//); + interceptor(fileRequest); + expect(fileRequest.headers['User-Agent']).toMatch(/^figmarine-rest\/git/); + }); + }); + describe('cacheInvalidationRequestInterceptor', () => { it('returns a function when called', ({ cache }) => { const interceptor = cacheInvalidationRequestInterceptor(cache); diff --git a/packages/rest/src/client.ts b/packages/rest/src/client.ts index 7666572..3a258c7 100644 --- a/packages/rest/src/client.ts +++ b/packages/rest/src/client.ts @@ -3,7 +3,11 @@ import { log } from '@figmarine/logger'; import { Api, type Api as ApiInterface } from './__generated__/figmaRestApi'; import { Cache, type ClientCacheOptions } from './cache'; -import { cacheInvalidationRequestInterceptor, rateLimitRequestInterceptor } from './interceptors'; +import { + cacheInvalidationRequestInterceptor, + rateLimitRequestInterceptor, + userAgentRequestInterceptor, +} from './interceptors'; import { get429Config } from './rateLimit.config'; import { securityWorker } from './securityWorker'; @@ -140,6 +144,9 @@ export async function Client(opts: ClientOptions = {}): Promise }); } + /* Add User-Agent header to all requests. */ + api.instance.interceptors.request.use(userAgentRequestInterceptor()); + log(`Created Figma REST API client successfully.`); return { diff --git a/packages/rest/src/interceptors.ts b/packages/rest/src/interceptors.ts index cc67c4f..606f3d2 100644 --- a/packages/rest/src/interceptors.ts +++ b/packages/rest/src/interceptors.ts @@ -3,6 +3,64 @@ import { log } from '@figmarine/logger'; import { Cache, generatePredictableKey } from './cache'; import { interceptRequest } from './rateLimit'; +import manifest from '../package.json'; + +function detectRuntime(): string { + if (typeof globalThis === 'undefined') { + return 'Unknown runtime (likely legacy)'; + } + + if ('Netlify' in globalThis) { + return 'netlify'; + } + + if ('__lagon__' in globalThis) { + return 'lagon'; + } + + if ('EdgeRuntime' in globalThis) { + return 'edge-light'; + } + + if ('fastly' in globalThis) { + return 'fastly'; + } + + // @ts-expect-error Runtime dependant global. + if ('Deno' in globalThis && globalThis.Deno.version.deno) { + // @ts-expect-error Runtime dependant global. + return `deno v${globalThis.Deno.version.deno}`; + } + + // @ts-expect-error Runtime dependant global. + if ('Bun' in globalThis && globalThis.Bun.version) { + // @ts-expect-error Runtime dependant global. + return `bun v${globalThis.Bun.version}`; + } + + if ('process' in globalThis && globalThis.process.versions?.node) { + return `node v${process.versions.node}`; + } + + if ('window' in globalThis) { + return 'Browser'; + } + + return 'unknown'; +} + +export function userAgentRequestInterceptor() { + const runtime = detectRuntime(); + const version = manifest.version.startsWith('0.0.0') ? 'git' : manifest.version; + const userAgent = `figmarine-rest/${version} (${runtime} runtime)`; + log(`Detected User-Agent: ${userAgent}.`); + + return function (config: InternalAxiosRequestConfig) { + config.headers = config.headers || {}; + config.headers['User-Agent'] = userAgent; + return config; + }; +} export function cacheInvalidationRequestInterceptor(cache: Cache) { return async function (config: InternalAxiosRequestConfig) {