diff --git a/.gitignore b/.gitignore index 0e75fe5..428fcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist coverage +.dev.vars* diff --git a/package.json b/package.json index f255d6e..e2c99ee 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "src/worker.js", "type": "module", "scripts": { - "build": "esbuild src/worker.js --bundle --format=esm --outfile=dist/worker.mjs", + "build": "esbuild src/worker.js --bundle --format=esm --external:node:buffer --external:node:events --external:node:async_hooks --outfile=dist/worker.mjs", "lint": "standard", "test": "npm run build && entail" }, @@ -25,8 +25,12 @@ "uint8arrays": "^5.1.0" }, "dependencies": { + "@microlabs/otel-cf-workers": "1.0.0-rc.49", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^1.30.0", "@web3-storage/gateway-lib": "^5.0.1", - "@web3-storage/public-bucket": "^1.2.0" + "@web3-storage/public-bucket": "^1.2.0", + "typescript": "^5.7.2" }, "repository": { "type": "git", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0837f5b..3ceb791 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,24 @@ importers: .: dependencies: + '@microlabs/otel-cf-workers': + specifier: 1.0.0-rc.49 + version: 1.0.0-rc.49(@opentelemetry/api@1.9.0) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/sdk-trace-base': + specifier: ^1.30.0 + version: 1.30.0(@opentelemetry/api@1.9.0) '@web3-storage/gateway-lib': specifier: ^5.0.1 version: 5.0.1(uglify-js@3.17.4) '@web3-storage/public-bucket': specifier: ^1.2.0 version: 1.2.0 + typescript: + specifier: ^5.7.2 + version: 5.7.2 devDependencies: '@cloudflare/workers-types': specifier: ^4.20240512.0 @@ -475,6 +487,11 @@ packages: resolution: {integrity: sha512-q8aKm0rhDxZjc4TzDpB0quog4pViFnz+Ok+UbGEk3xXxHwT3QCxaDVPKMemMqN/1N3OahVvcodpcvFSuWmus+A==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@microlabs/otel-cf-workers@1.0.0-rc.49': + resolution: {integrity: sha512-wPfaHxFAOOHlLxvWVGhdsMf+lcSsjtysfHOwk7hnTdUdim1f3trTVxaa0fl7xF/AdZ1LgGbLI9xbCEz14qSzNw==} + peerDependencies: + '@opentelemetry/api': ~1.9.0 + '@miyauci/isx@1.1.1': resolution: {integrity: sha512-I675QDyEPVM8Ya/Yfr908YxhFMyoht6xysqKmEmkuohheyTs1ZO6mzm6tI/ddKfGNDymatwqiMtqfXjLI0seWg==} @@ -526,6 +543,88 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.53.0': + resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@1.26.0': + resolution: {integrity: sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.0': + resolution: {integrity: sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.53.0': + resolution: {integrity: sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-exporter-base@0.53.0': + resolution: {integrity: sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-transformer@0.53.0': + resolution: {integrity: sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@1.26.0': + resolution: {integrity: sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.30.0': + resolution: {integrity: sha512-5mGMjL0Uld/99t7/pcd7CuVtJbkARckLVuiOX84nO8RtLtIz0/J6EOHM2TGvPZ6F4K+XjUq13gMx14w80SVCQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.53.0': + resolution: {integrity: sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.26.0': + resolution: {integrity: sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.26.0': + resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.0': + resolution: {integrity: sha512-RKQDaDIkV7PwizmHw+rE/FgfB2a6MBx+AEVVlAHXRG1YYxLiBpPX2KhmoB99R5vA4b72iJrjle68NDWnbrE9Dg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.27.0': + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -2199,6 +2298,11 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -2952,6 +3056,17 @@ snapshots: - supports-color - utf-8-validate + '@microlabs/otel-cf-workers@1.0.0-rc.49(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + '@miyauci/isx@1.1.1': {} '@miyauci/prelude@1.0.0': {} @@ -3025,6 +3140,91 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@opentelemetry/api-logs@0.53.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-trace-otlp-http@0.53.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.53.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.53.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.53.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0) + protobufjs: 7.3.0 + + '@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/resources@1.30.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.53.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/semantic-conventions@1.27.0': {} + + '@opentelemetry/semantic-conventions@1.28.0': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5083,6 +5283,8 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + typescript@5.7.2: {} + uglify-js@3.17.4: {} uint8-varint@1.0.8: diff --git a/src/tracebucket.js b/src/tracebucket.js new file mode 100644 index 0000000..43f71b2 --- /dev/null +++ b/src/tracebucket.js @@ -0,0 +1,67 @@ +// eslint-disable-next-line +import * as BucketAPI from '@web3-storage/public-bucket' +import { trace, context, SpanStatusCode } from '@opentelemetry/api' + +/** + * @template {unknown[]} A + * @template {*} T + * @template {*} This + * @param {string} spanName + * @param {(this: This, ...args: A) => Promise} fn + * @param {This} [thisParam] + */ +const withSimpleSpan = (spanName, fn, thisParam) => + /** + * @param {A} args + */ + async (...args) => { + const tracer = trace.getTracer('public-r2-bucket') + const span = tracer.startSpan(spanName) + const ctx = trace.setSpan(context.active(), span) + + try { + const result = await context.with(ctx, fn, thisParam, ...args) + span.setStatus({ code: SpanStatusCode.OK }) + span.end() + return result + } catch (err) { + if (err instanceof Error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message + }) + } else { + span.setStatus({ + code: SpanStatusCode.ERROR + }) + } + span.end() + throw err + } + } + +/** @implements {BucketAPI.Bucket} */ +export class TraceBucket { + #bucket + + /** + * + * @param {BucketAPI.Bucket} bucket + */ + constructor (bucket) { + this.#bucket = bucket + } + + /** @param {string} key */ + head (key) { + return withSimpleSpan('bucket.head', this.#bucket.head, this.#bucket)(key) + } + + /** + * @param {string} key + * @param {BucketAPI.GetOptions} [options] + */ + get (key, options) { + return withSimpleSpan('bucket.get', this.#bucket.get, this.#bucket)(key, options) + } +} diff --git a/src/worker.d.ts b/src/worker.d.ts index 166aa13..64184b5 100644 --- a/src/worker.d.ts +++ b/src/worker.d.ts @@ -1,9 +1,12 @@ import { R2Bucket } from '@cloudflare/workers-types' +import { Environment as MiddlewareEnvironment } from '@web3-storage/gateway-lib' export { R2Bucket } -export interface Environment { +export interface Environment extends MiddlewareEnvironment { BUCKET: R2Bucket + FF_TELEMETRY_ENABLED: string + HONEYCOMB_API_KEY: string } export declare function handler (request: Request, env: Environment): Promise diff --git a/src/worker.js b/src/worker.js index e8e1ccc..4e06812 100644 --- a/src/worker.js +++ b/src/worker.js @@ -7,6 +7,22 @@ import { composeMiddleware } from '@web3-storage/gateway-lib/middleware' import { createHandler } from '@web3-storage/public-bucket/server' +import { instrument } from '@microlabs/otel-cf-workers' +import { AlwaysOffSampler, NoopSpanProcessor, ParentBasedSampler } from '@opentelemetry/sdk-trace-base' +import { TraceBucket } from './tracebucket.js' + +/** + * @import { + * Handler, + * Middleware, + * Context, + * IpfsUrlContext, + * BlockContext, + * DagContext, + * UnixfsContext + * } from '@web3-storage/gateway-lib' + * @import { Environment } from './worker.d.ts' + */ /** * 20MiB should allow the worker to process ~4-5 concurrent requests that @@ -14,18 +30,73 @@ import { createHandler } from '@web3-storage/public-bucket/server' */ const MAX_BATCH_SIZE = 20 * 1024 * 1024 +/** + * The promise to the pre-configured handler + * + * @type {Promise> | null} + */ +let handlerPromise = null + +/** + * Pre-configure the handler based on the environment. + * + * @param {Environment} env + * @returns {Promise>} + */ +async function initializeHandler (env) { + const bucket = new TraceBucket(/** @type {import('@web3-storage/public-bucket').Bucket} */ (env.BUCKET)) + const handler = createHandler({ bucket, maxBatchSize: MAX_BATCH_SIZE }) + const middleware = composeMiddleware( + withCdnCache, + withContext, + withCorsHeaders, + withFixedLengthStream + ) + const baseHandler = middleware(handler) + if (env.FF_TELEMETRY_ENABLED === 'true') { + globalThis.fetch = globalThis.fetch.bind(globalThis) + } + const finalHandler = env.FF_TELEMETRY_ENABLED === 'true' + ? /** @type {Handler} */(instrument({ fetch: baseHandler }, config).fetch) + : baseHandler + return finalHandler +} + +/** + * Configure the OpenTelemetry exporter based on the environment + * + * @param {Environment} env + * @param {*} _trigger + * @returns {import('@microlabs/otel-cf-workers').TraceConfig} + */ +function config (env, _trigger) { + if (env.HONEYCOMB_API_KEY) { + return { + exporter: { + url: 'https://api.honeycomb.io/v1/traces', + headers: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY } + }, + service: { name: 'freeway' }, + sampling: { + headSampler: new ParentBasedSampler({ root: new AlwaysOffSampler() }) + } + } + } + return { + spanProcessors: new NoopSpanProcessor(), + service: { name: 'freeway' } + } +} + export default { /** @type {import('@web3-storage/gateway-lib').Handler} */ - fetch (request, env, ctx) { + async fetch (request, env, ctx) { console.log(request.method, request.url) - const bucket = /** @type {import('@web3-storage/public-bucket').Bucket} */ (env.BUCKET) - const handler = createHandler({ bucket, maxBatchSize: MAX_BATCH_SIZE }) - const middleware = composeMiddleware( - withCdnCache, - withContext, - withCorsHeaders, - withFixedLengthStream - ) - return middleware(handler)(request, env, ctx) + // Initialize the handler only once and reuse the promise + if (!handlerPromise) { + handlerPromise = initializeHandler(env) + } + const handler = await handlerPromise + return handler(request, env, ctx) } } diff --git a/test/worker.spec.js b/test/worker.spec.js index 0e3d0e4..d4e4083 100644 --- a/test/worker.spec.js +++ b/test/worker.spec.js @@ -6,7 +6,8 @@ import * as ByteRanges from 'byteranges' const mf = new Miniflare({ modules: true, scriptPath: `${import.meta.dirname}/../dist/worker.mjs`, - compatibilityDate: '2024-05-15', + compatibilityFlags: ['nodejs_compat'], + compatibilityDate: '2024-09-23', r2Buckets: ['BUCKET'] }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7fa3406 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["src", "test", "scripts"], + "exclude": ["scripts/r2-put.js"], + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "allowJs": true, + "checkJs": true, + "strict": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "target": "ES2022", + "sourceMap": true + } +} diff --git a/wrangler.toml b/wrangler.toml index 130bbde..8489071 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,6 +1,7 @@ name = "public-bucket" main = "./dist/worker.mjs" -compatibility_date = "2024-05-15" +compatibility_flags = [ "nodejs_compat" ] +compatibility_date = "2024-09-23" r2_buckets = [{ binding = "BUCKET", bucket_name = "carpark-dev-0" }] [build] @@ -17,9 +18,24 @@ route = { pattern = "https://carpark-prod-0.r2.w3s.link/*", zone_id = "ae60d8f73 [env.carpark-production.vars] DEBUG = "false" +FF_TELEMETRY_ENABLED = true # STAGING! [env.carpark-staging] account_id = "fffa4b4363a7e5250af8357087263b3a" r2_buckets = [{ binding = "BUCKET", bucket_name = "carpark-staging-0" }] route = { pattern = "https://carpark-staging-0.r2.w3s.link/*", zone_id = "ae60d8f737317467ec666dc3851a6277" } + +[env.carpark-staging.vars] +FF_TELEMETRY_ENABLED = true +DEBUG = "true" + +# HANNAH DEV TESTING! +[env.hannahhoward] +account_id = "fffa4b4363a7e5250af8357087263b3a" +r2_buckets = [{ binding = "BUCKET", bucket_name = "carpark-prod-0", preview_bucket_name = "carpark-prod-0" }] +route = { pattern = "https://carpark-hannah-0.r2.w3s.link/*", zone_id = "ae60d8f737317467ec666dc3851a6277" } + +[env.hannahhoward.vars] +FF_TELEMETRY_ENABLED = "true" +DEBUG = "true" \ No newline at end of file