From 16ded23dc1528776fc964cac42cec38f121c7a5f Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 11 Oct 2023 12:35:49 +0100 Subject: [PATCH 01/13] feat(remix): Add Worker Runtime support to Remix SDK. --- packages/remix/package.json | 6 +- packages/remix/src/index.client.tsx | 57 ++++++- packages/remix/src/index.server.ts | 1 + packages/remix/src/utils/instrumentServer.ts | 11 +- .../remix/src/utils/serverAdapters/worker.ts | 83 ++++++++++ .../remix/src/utils/vendor/getIpAddress.ts | 3 +- packages/remix/src/utils/vendor/types.ts | 2 +- packages/remix/src/worker/transport.ts | 103 ++++++++++++ yarn.lock | 148 ++++++++++-------- 9 files changed, 343 insertions(+), 71 deletions(-) create mode 100644 packages/remix/src/utils/serverAdapters/worker.ts create mode 100644 packages/remix/src/worker/transport.ts diff --git a/packages/remix/package.json b/packages/remix/package.json index 19eec703b3e9..32ca8615a2be 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -34,11 +34,13 @@ "@sentry/types": "7.81.1", "@sentry/utils": "7.81.1", "glob": "^10.3.4", + "is-ip": "^3.1.0", + "tslib": "^2.4.1 || ^1.9.3", "yargs": "^17.6.0" }, "devDependencies": { - "@remix-run/node": "^1.4.3", - "@remix-run/react": "^1.4.3", + "@remix-run/node": "^1.19.3", + "@remix-run/react": "^1.19.3", "@types/express": "^4.17.14" }, "peerDependencies": { diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index 64951a3f10cd..a817450c2615 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -1,11 +1,28 @@ /* eslint-disable import/export */ -import { configureScope, init as reactInit } from '@sentry/react'; +import { init as reactInit } from '@sentry/react'; import { buildMetadata } from './utils/metadata'; import type { RemixOptions } from './utils/remixOptions'; export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; export * from '@sentry/react'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { + configureScope, + getCurrentHub, + getIntegrationsToSetup, + initAndBind, + ServerRuntimeClient, + startTransaction, +} from '@sentry/core'; +import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; + +import { makeEdgeTransport } from './worker/transport'; + +export { captureRemixServerException } from './utils/instrumentServer'; +export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; +// export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; +export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker'; export function init(options: RemixOptions): void { buildMetadata(options, ['remix', 'react']); @@ -17,3 +34,41 @@ export function init(options: RemixOptions): void { scope.setTag('runtime', 'browser'); }); } + +const nodeStackParser = createStackParser(nodeStackLineParser()); + +function sdkAlreadyInitialized(): boolean { + const hub = getCurrentHub(); + return !!hub.getClient(); +} + +/** Initializes Sentry Remix SDK on Node. */ +export function workerInit(options: RemixOptions): void { + buildMetadata(options, ['remix', 'node']); + + if (sdkAlreadyInitialized()) { + __DEBUG_BUILD__ && logger.log('SDK already initialized'); + + return; + } + + const clientOptions: ServerRuntimeClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeEdgeTransport, + }; + + initAndBind(ServerRuntimeClient, clientOptions); + + configureScope(scope => { + scope.setTag('runtime', 'worker'); + }); + + const transaction = startTransaction({ + name: 'remix-main', + op: 'init', + }); + + transaction.finish(); +} diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 80c6602e4938..36261f1bc011 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -62,6 +62,7 @@ export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; +export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker'; export type { SentryMetaArgs } from './utils/types'; diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 935e24124b49..8f462c0d596d 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,7 +1,12 @@ /* eslint-disable max-lines */ -import { getActiveTransaction, hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; -import type { Hub } from '@sentry/node'; -import { captureException, getCurrentHub } from '@sentry/node'; +import type { Hub } from '@sentry/core'; +import { + captureException, + getActiveTransaction, + getCurrentHub, + hasTracingEnabled, + runWithAsyncContext, +} from '@sentry/core'; import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types'; import { addExceptionMechanism, diff --git a/packages/remix/src/utils/serverAdapters/worker.ts b/packages/remix/src/utils/serverAdapters/worker.ts new file mode 100644 index 000000000000..88832183f36e --- /dev/null +++ b/packages/remix/src/utils/serverAdapters/worker.ts @@ -0,0 +1,83 @@ +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; +import { isString, logger } from '@sentry/utils'; + +import { + createRoutes, + getTransactionName, + instrumentBuild, + isRequestHandlerWrapped, + startRequestHandlerTransaction, +} from '../instrumentServer'; +import type { ReactRouterDomPkg, ServerBuild } from '../vendor/types'; + +type WorkerRequestHandler = (request: Request) => Promise; +export type WorkerCreateRequestHandler = (this: unknown, options: any) => WorkerRequestHandler; +type WorkerRequestHandlerOptions = { + build: ServerBuild; + mode?: string; + poweredByHeader?: boolean; + getLoadContext?: (request: Request) => Promise | unknown; +}; + +let pkg: ReactRouterDomPkg; + +function wrapWorkerRequestHandler(origRequestHandler: WorkerRequestHandler, build: ServerBuild): WorkerRequestHandler { + const routes = createRoutes(build.routes); + + // If the core request handler is already wrapped, don't wrap Express handler which uses it. + if (isRequestHandlerWrapped) { + return origRequestHandler; + } + + return async function (this: unknown, request: Request): Promise { + if (!pkg) { + try { + pkg = await import('react-router-dom'); + } finally { + if (!pkg) { + __DEBUG_BUILD__ && logger.error('Could not find `react-router-dom` package.'); + } + } + } + + const hub = getCurrentHub(); + const options = hub.getClient()?.getOptions(); + const scope = hub.getScope(); + + scope.setSDKProcessingMetadata({ request }); + + if (!options || !hasTracingEnabled(options) || !request.url || !request.method) { + return origRequestHandler.call(this, request); + } + + const url = new URL(request.url); + const [name, source] = getTransactionName(routes, url, pkg); + startRequestHandlerTransaction(hub, name, source, { + headers: { + 'sentry-trace': + (request.headers && isString(request.headers.get('sentry-trace')) && request.headers.get('sentry-trace')) || + '', + baggage: (request.headers && isString(request.headers.get('baggage')) && request.headers.get('baggage')) || '', + }, + method: request.method, + }); + + return origRequestHandler.call(this, request); + }; +} + +/** + * Instruments `createRequestHandler` from `@remix-run/express` + */ +export function wrapWorkerCreateRequestHandler( + origCreateRequestHandler: WorkerCreateRequestHandler, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (options: any) => WorkerRequestHandler { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (this: unknown, options: any): WorkerRequestHandler { + const newBuild = instrumentBuild((options as WorkerRequestHandlerOptions).build); + const requestHandler = origCreateRequestHandler.call(this, { ...options, build: newBuild }); + + return wrapWorkerRequestHandler(requestHandler, newBuild); + }; +} diff --git a/packages/remix/src/utils/vendor/getIpAddress.ts b/packages/remix/src/utils/vendor/getIpAddress.ts index d63e31779aac..52fef0db1f7f 100644 --- a/packages/remix/src/utils/vendor/getIpAddress.ts +++ b/packages/remix/src/utils/vendor/getIpAddress.ts @@ -23,8 +23,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { isIP } from 'net'; - +import isIP from 'is-ip'; /** * Get the IP address of the client sending a request. * diff --git a/packages/remix/src/utils/vendor/types.ts b/packages/remix/src/utils/vendor/types.ts index faaa7e5f6f60..da5c2a19b595 100644 --- a/packages/remix/src/utils/vendor/types.ts +++ b/packages/remix/src/utils/vendor/types.ts @@ -202,7 +202,7 @@ export interface DataFunction { } export interface ReactRouterDomPkg { - matchRoutes: (routes: ServerRoute[], pathname: string) => RouteMatch[] | null; + matchRoutes: (routes: any[], pathname: string) => RouteMatch[] | null; } // Taken from Remix Implementation diff --git a/packages/remix/src/worker/transport.ts b/packages/remix/src/worker/transport.ts new file mode 100644 index 000000000000..a479425f96e6 --- /dev/null +++ b/packages/remix/src/worker/transport.ts @@ -0,0 +1,103 @@ +import { createTransport } from '@sentry/core'; +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import { SentryError } from '@sentry/utils'; + +export interface VercelEdgeTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. */ + headers?: { [key: string]: string }; +} + +const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; + +/** + * This is a modified promise buffer that collects tasks until drain is called. + * We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests + * and responses, and the normal PromiseBuffer inherently buffers stuff inbetween incoming requests. + * + * A limitation we need to be aware of is that DEFAULT_TRANSPORT_BUFFER_SIZE is the maximum amount of payloads the + * SDK can send for a given edge function invocation. + */ +export class IsolatedPromiseBuffer { + // We just have this field because the promise buffer interface requires it. + // If we ever remove it from the interface we should also remove it here. + public $: Array>; + + private _taskProducers: (() => PromiseLike)[]; + + private readonly _bufferSize: number; + + public constructor(_bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE) { + this.$ = []; + this._taskProducers = []; + this._bufferSize = _bufferSize; + } + + /** + * @inheritdoc + */ + public add(taskProducer: () => PromiseLike): PromiseLike { + if (this._taskProducers.length >= this._bufferSize) { + return Promise.reject(new SentryError('Not adding Promise because buffer limit was reached.')); + } + + this._taskProducers.push(taskProducer); + return Promise.resolve(); + } + + /** + * @inheritdoc + */ + public drain(timeout?: number): PromiseLike { + const oldTaskProducers = [...this._taskProducers]; + this._taskProducers = []; + + return new Promise(resolve => { + const timer = setTimeout(() => { + if (timeout && timeout > 0) { + resolve(false); + } + }, timeout); + + void Promise.all( + oldTaskProducers.map(taskProducer => + taskProducer().then(null, () => { + // catch all failed requests + }), + ), + ).then(() => { + // resolve to true if all fetch requests settled + clearTimeout(timer); + resolve(true); + }); + }); + } +} + +/** + * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. + */ +export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions, + }; + + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); + } + + return createTransport(options, makeRequest, new IsolatedPromiseBuffer(options.bufferSize)); +} diff --git a/yarn.lock b/yarn.lock index d32acb0b89ae..d1e6f43f1900 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4740,85 +4740,91 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@remix-run/node@^1.4.3": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-1.5.1.tgz#1c367d4035baaef8f0ea66962a826456d62f0030" - integrity sha512-yl4bd1nl7MiJp4tI3+4ygObeMU3txM4Uo09IdHLRa4NMdBQnacUJ47kqCahny01MerC2JL2d9NPjdVPwRCRZvQ== - dependencies: - "@remix-run/server-runtime" "1.5.1" - "@remix-run/web-fetch" "^4.1.3" - "@remix-run/web-file" "^3.0.2" - "@remix-run/web-stream" "^1.0.3" +"@remix-run/node@^1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-1.19.3.tgz#d27e2f742fc45379525cb3fca466a883ca06d6c9" + integrity sha512-z5qrVL65xLXIUpU4mkR4MKlMeKARLepgHAk4W5YY3IBXOreRqOGUC70POViYmY7x38c2Ia1NwqL80H+0h7jbMw== + dependencies: + "@remix-run/server-runtime" "1.19.3" + "@remix-run/web-fetch" "^4.3.6" + "@remix-run/web-file" "^3.0.3" + "@remix-run/web-stream" "^1.0.4" "@web3-storage/multipart-parser" "^1.0.0" abort-controller "^3.0.0" cookie-signature "^1.1.0" source-map-support "^0.5.21" stream-slice "^0.1.2" -"@remix-run/react@^1.4.3": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-1.5.1.tgz#372e5e80f3f10a638b0567c4e03307dfb0a28dc0" - integrity sha512-p4t6tC/WyPeLW7DO4g7ZSyH9EpWO37c4wD2np3rDwtv3WtsTZ70bU/+NOWE9nv74mH8i1C50eJ3/OR+8Ll8UbA== +"@remix-run/react@^1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-1.19.3.tgz#00efcc583bf05b434566e56381d51df86575d8b0" + integrity sha512-iP37MZ+oG1n4kv4rX77pKT/knra51lNwKo5tinPPF0SuNJhF3+XjWo5nwEjvisKTXLZ/OHeicinhgX2JHHdDvA== dependencies: - history "^5.3.0" - react-router-dom "^6.2.2" + "@remix-run/router" "1.7.2" + react-router-dom "6.14.2" "@remix-run/router@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.2.tgz#1c17eadb2fa77f80a796ad5ea9bf108e6993ef06" integrity sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ== -"@remix-run/server-runtime@1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@remix-run/server-runtime/-/server-runtime-1.5.1.tgz#5272b01e6dce109dc10bd68447ceae2d039315b2" - integrity sha512-FQbCCdW+qzE3wpoCwUKdwcL8yZVYNPiyHS9JS/6r6qmd/yvZfbj44E48wEQ6trbWE2TUiEh/EQqNMyrZWEs4bw== +"@remix-run/router@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8" + integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A== + +"@remix-run/server-runtime@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@remix-run/server-runtime/-/server-runtime-1.19.3.tgz#206b55337c266c5bc254878f8ff3cd5677cc60fb" + integrity sha512-KzQ+htUsKqpBgKE2tWo7kIIGy3MyHP58Io/itUPvV+weDjApwr9tQr9PZDPA3yAY6rAzLax7BU0NMSYCXWFY5A== dependencies: - "@types/cookie" "^0.4.0" + "@remix-run/router" "1.7.2" + "@types/cookie" "^0.4.1" "@web3-storage/multipart-parser" "^1.0.0" cookie "^0.4.1" - jsesc "^3.0.1" - react-router-dom "^6.2.2" set-cookie-parser "^2.4.8" source-map "^0.7.3" -"@remix-run/web-blob@^3.0.3", "@remix-run/web-blob@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.0.4.tgz#99c67b9d0fb641bd0c07d267fd218ae5aa4ae5ed" - integrity sha512-AfegzZvSSDc+LwnXV+SwROTrDtoLiPxeFW+jxgvtDAnkuCX1rrzmVJ6CzqZ1Ai0bVfmJadkG5GxtAfYclpPmgw== +"@remix-run/web-blob@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.1.0.tgz#e0c669934c1eb6028960047e57a13ed38bbfb434" + integrity sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g== dependencies: - "@remix-run/web-stream" "^1.0.0" + "@remix-run/web-stream" "^1.1.0" web-encoding "1.1.5" -"@remix-run/web-fetch@^4.1.3": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@remix-run/web-fetch/-/web-fetch-4.1.3.tgz#8ad3077c1b5bd9fe2a8813d0ad3c84970a495c04" - integrity sha512-D3KXAEkzhR248mu7wCHReQrMrIo3Y9pDDa7TrlISnsOEvqkfWkJJF+PQWmOIKpOSHAhDg7TCb2tzvW8lc/MfHw== +"@remix-run/web-fetch@^4.3.6": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@remix-run/web-fetch/-/web-fetch-4.4.1.tgz#1ea34e6f1c660a52e7582007917a552f0efdc58b" + integrity sha512-xMceEGn2kvfeWS91nHSOhEQHPGgjFnmDVpWFZrbWPVdiTByMZIn421/tdSF6Kd1RsNsY+5Iwt3JFEKZHAcMQHw== dependencies: - "@remix-run/web-blob" "^3.0.4" - "@remix-run/web-form-data" "^3.0.2" - "@remix-run/web-stream" "^1.0.3" + "@remix-run/web-blob" "^3.1.0" + "@remix-run/web-file" "^3.1.0" + "@remix-run/web-form-data" "^3.1.0" + "@remix-run/web-stream" "^1.1.0" "@web3-storage/multipart-parser" "^1.0.0" + abort-controller "^3.0.0" data-uri-to-buffer "^3.0.1" mrmime "^1.0.0" -"@remix-run/web-file@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@remix-run/web-file/-/web-file-3.0.2.tgz#1a6cc0900a1310ede4bc96abad77ac6eb27a2131" - integrity sha512-eFC93Onh/rZ5kUNpCQersmBtxedGpaXK2/gsUl49BYSGK/DvuPu3l06vmquEDdcPaEuXcsdGP0L7zrmUqrqo4A== +"@remix-run/web-file@^3.0.3", "@remix-run/web-file@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-file/-/web-file-3.1.0.tgz#07219021a2910e90231bc30ca1ce693d0e9d3825" + integrity sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ== dependencies: - "@remix-run/web-blob" "^3.0.3" + "@remix-run/web-blob" "^3.1.0" -"@remix-run/web-form-data@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@remix-run/web-form-data/-/web-form-data-3.0.2.tgz#733a4c8f8176523b7b60a8bd0dc6704fd4d498f3" - integrity sha512-F8tm3iB1sPxMpysK6Js7lV3gvLfTNKGmIW38t/e6dtPEB5L1WdbRG1cmLyhsonFc7rT1x1JKdz+2jCtoSdnIUw== +"@remix-run/web-form-data@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz#47f9ad8ce8bf1c39ed83eab31e53967fe8e3df6a" + integrity sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A== dependencies: web-encoding "1.1.5" -"@remix-run/web-stream@^1.0.0", "@remix-run/web-stream@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@remix-run/web-stream/-/web-stream-1.0.3.tgz#3284a6a45675d1455c4d9c8f31b89225c9006438" - integrity sha512-wlezlJaA5NF6SsNMiwQnnAW6tnPzQ5I8qk0Y0pSohm0eHKa2FQ1QhEKLVVcDDu02TmkfHgnux0igNfeYhDOXiA== +"@remix-run/web-stream@^1.0.4", "@remix-run/web-stream@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-stream/-/web-stream-1.1.0.tgz#b93a8f806c2c22204930837c44d81fdedfde079f" + integrity sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA== dependencies: web-streams-polyfill "^3.1.1" @@ -5559,7 +5565,7 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.2.tgz#9bf9d62c838c85a07c92fdf2334c2c14fd9c59a9" integrity sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA== -"@types/cookie@^0.4.0", "@types/cookie@^0.4.1": +"@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== @@ -16806,7 +16812,7 @@ history@^4.6.0, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -history@^5.2.0, history@^5.3.0: +history@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== @@ -17551,6 +17557,11 @@ ip-regex@^2.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= +ip-regex@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + ip@1.1.5, ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -17852,6 +17863,13 @@ is-interactive@^2.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== +is-ip@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8" + integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q== + dependencies: + ip-regex "^4.0.0" + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -19010,11 +19028,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - jsesc@~0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.3.0.tgz#1bf5ee63b4539fe2e26d0c1e99c240b97a457972" @@ -25725,21 +25738,27 @@ react-refresh@0.8.3: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: - name react-router-6 +"react-router-6@npm:react-router@6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== dependencies: history "^5.2.0" -react-router-dom@^6.2.2: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" - integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== +react-router-dom@6.14.2: + version "6.14.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.2.tgz#88f520118b91aa60233bd08dbd3fdcaea3a68488" + integrity sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg== dependencies: - history "^5.2.0" - react-router "6.3.0" + "@remix-run/router" "1.7.2" + react-router "6.14.2" + +react-router@6.14.2: + version "6.14.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300" + integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ== + dependencies: + "@remix-run/router" "1.7.2" react@^18.0.0: version "18.0.0" @@ -29722,6 +29741,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== +"tslib@^2.4.1 || ^1.9.3": + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 3109b711e952076b5da756e5b0298c994381c21e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 12 Oct 2023 21:08:16 +0100 Subject: [PATCH 02/13] Update `isBrowser` checks. --- packages/remix/src/client/errors.tsx | 5 +-- packages/remix/src/client/performance.tsx | 6 ++-- packages/remix/src/index.client.tsx | 35 ++++++-------------- packages/remix/src/utils/futureFlags.ts | 14 +++++++- packages/remix/src/utils/instrumentServer.ts | 5 ++- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/remix/src/client/errors.tsx b/packages/remix/src/client/errors.tsx index a6afefbc0ef7..a36d0050777d 100644 --- a/packages/remix/src/client/errors.tsx +++ b/packages/remix/src/client/errors.tsx @@ -1,6 +1,7 @@ import { captureException } from '@sentry/core'; -import { isNodeEnv, isString } from '@sentry/utils'; +import { isString } from '@sentry/utils'; +import { isBrowser } from '../utils/futureFlags'; import { isRouteErrorResponse } from '../utils/vendor/response'; import type { ErrorResponse } from '../utils/vendor/types'; @@ -12,7 +13,7 @@ import type { ErrorResponse } from '../utils/vendor/types'; */ export function captureRemixErrorBoundaryError(error: unknown): string | undefined { let eventId: string | undefined; - const isClientSideRuntimeError = !isNodeEnv() && error instanceof Error; + const isClientSideRuntimeError = isBrowser() && error instanceof Error; const isRemixErrorResponse = isRouteErrorResponse(error); // Server-side errors apart from `ErrorResponse`s also appear here without their stacktraces. // So, we only capture: diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 97c455a80a94..6fa4e4393a42 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -1,10 +1,10 @@ import type { ErrorBoundaryProps } from '@sentry/react'; import { WINDOW, withErrorBoundary } from '@sentry/react'; import type { Transaction, TransactionContext } from '@sentry/types'; -import { isNodeEnv, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import * as React from 'react'; -import { getFutureFlagsBrowser, readRemixVersionFromLoader } from '../utils/futureFlags'; +import { getFutureFlagsBrowser, isBrowser, readRemixVersionFromLoader } from '../utils/futureFlags'; const DEFAULT_TAGS = { 'routing.instrumentation': 'remix-router', @@ -109,7 +109,7 @@ export function withSentry

, R extends React.Co // Early return when any of the required functions is not available. if (!_useEffect || !_useLocation || !_useMatches || !_customStartTransaction) { __DEBUG_BUILD__ && - !isNodeEnv() && + isBrowser() && logger.warn('Remix SDK was unable to wrap your root because of one or more missing parameters.'); // @ts-expect-error Setting more specific React Component typing for `R` generic above diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index a817450c2615..7dbc156fca6a 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -7,23 +7,22 @@ export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; export * from '@sentry/react'; import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { - configureScope, - getCurrentHub, - getIntegrationsToSetup, - initAndBind, - ServerRuntimeClient, - startTransaction, -} from '@sentry/core'; +import { configureScope, getCurrentHub, getIntegrationsToSetup, initAndBind, ServerRuntimeClient } from '@sentry/core'; import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { makeEdgeTransport } from './worker/transport'; export { captureRemixServerException } from './utils/instrumentServer'; export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; -// export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker'; +const nodeStackParser = createStackParser(nodeStackLineParser()); + +function sdkAlreadyInitialized(): boolean { + const hub = getCurrentHub(); + return !!hub.getClient(); +} + export function init(options: RemixOptions): void { buildMetadata(options, ['remix', 'react']); options.environment = options.environment || process.env.NODE_ENV; @@ -35,16 +34,9 @@ export function init(options: RemixOptions): void { }); } -const nodeStackParser = createStackParser(nodeStackLineParser()); - -function sdkAlreadyInitialized(): boolean { - const hub = getCurrentHub(); - return !!hub.getClient(); -} - -/** Initializes Sentry Remix SDK on Node. */ +/** Initializes Sentry Remix SDK on Worker Environments. */ export function workerInit(options: RemixOptions): void { - buildMetadata(options, ['remix', 'node']); + buildMetadata(options, ['remix', 'worker']); if (sdkAlreadyInitialized()) { __DEBUG_BUILD__ && logger.log('SDK already initialized'); @@ -64,11 +56,4 @@ export function workerInit(options: RemixOptions): void { configureScope(scope => { scope.setTag('runtime', 'worker'); }); - - const transaction = startTransaction({ - name: 'remix-main', - op: 'init', - }); - - transaction.finish(); } diff --git a/packages/remix/src/utils/futureFlags.ts b/packages/remix/src/utils/futureFlags.ts index 03d03270a73e..91afbf658ea3 100644 --- a/packages/remix/src/utils/futureFlags.ts +++ b/packages/remix/src/utils/futureFlags.ts @@ -1,4 +1,4 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; +import { GLOBAL_OBJ, isNodeEnv } from '@sentry/utils'; import type { FutureConfig, ServerBuild } from './vendor/types'; @@ -65,3 +65,15 @@ export function readRemixVersionFromLoader(): number | undefined { return window.__remixContext?.state?.loaderData?.root?.remixVersion; } + +/** + * Check if we are in the browser + * Checking the existence of document instead of window + * See:https://remix.run/docs/en/1.19.3/pages/gotchas#typeof-window-checks + * + * @returns True if we are in the browser + */ +export function isBrowser(): boolean { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-restricted-globals + return typeof document !== 'undefined' && !isNodeEnv(); +} diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 8f462c0d596d..c027fd4986f6 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -12,13 +12,12 @@ import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, fill, - isNodeEnv, loadModule, logger, tracingContextFromHeaders, } from '@sentry/utils'; -import { getFutureFlagsServer, getRemixVersionFromBuild } from './futureFlags'; +import { getFutureFlagsServer, getRemixVersionFromBuild, isBrowser } from './futureFlags'; import { extractData, getRequestMatch, @@ -241,7 +240,7 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string } const transaction = getActiveTransaction(); const currentScope = getCurrentHub().getScope(); - if (isNodeEnv() && hasTracingEnabled()) { + if (!isBrowser() && hasTracingEnabled()) { const span = currentScope.getSpan(); if (span && transaction) { From 793298906ac26f56e60e66b8bd2ba928f965ef70 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 13 Oct 2023 01:42:18 +0100 Subject: [PATCH 03/13] Add Cloudflare Worker transport and adapter. --- packages/remix/src/index.client.tsx | 5 + packages/remix/src/index.server.ts | 1 + .../utils/serverAdapters/cloudflare-worker.ts | 85 +++++++++++++++++ packages/remix/src/worker/transport.ts | 94 +++---------------- 4 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 packages/remix/src/utils/serverAdapters/cloudflare-worker.ts diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index 7dbc156fca6a..06fdf8740d32 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -15,6 +15,7 @@ import { makeEdgeTransport } from './worker/transport'; export { captureRemixServerException } from './utils/instrumentServer'; export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker'; +export { wrapCloudflareWorkerCreateRequestHandler } from './utils/serverAdapters/cloudflare-worker'; const nodeStackParser = createStackParser(nodeStackLineParser()); @@ -36,6 +37,8 @@ export function init(options: RemixOptions): void { /** Initializes Sentry Remix SDK on Worker Environments. */ export function workerInit(options: RemixOptions): void { + console.log('WORKER INIT', options); + buildMetadata(options, ['remix', 'worker']); if (sdkAlreadyInitialized()) { @@ -44,6 +47,8 @@ export function workerInit(options: RemixOptions): void { return; } + console.log('options.transport', options.transport); + const clientOptions: ServerRuntimeClientOptions = { ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 36261f1bc011..b66c61ff07ff 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -63,6 +63,7 @@ export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker'; +export { wrapCloudflareWorkerCreateRequestHandler } from './utils/serverAdapters/cloudflare-worker'; export type { SentryMetaArgs } from './utils/types'; diff --git a/packages/remix/src/utils/serverAdapters/cloudflare-worker.ts b/packages/remix/src/utils/serverAdapters/cloudflare-worker.ts new file mode 100644 index 000000000000..61c43d274a55 --- /dev/null +++ b/packages/remix/src/utils/serverAdapters/cloudflare-worker.ts @@ -0,0 +1,85 @@ +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; +import { isString, logger } from '@sentry/utils'; + +import { + createRoutes, + getTransactionName, + instrumentBuild, + isRequestHandlerWrapped, + startRequestHandlerTransaction, +} from '../instrumentServer'; +import type { ReactRouterDomPkg, ServerBuild } from '../vendor/types'; + +type WorkerRequestHandler = (request: Request) => Promise; + +export type CloudflareWorkerCreateRequestHandler = ( + this: unknown, + build: ServerBuild, + mode?: string, +) => WorkerRequestHandler; + +let pkg: ReactRouterDomPkg; + +function wrapCloudflareWorkerRequestHandler( + origRequestHandler: WorkerRequestHandler, + build: ServerBuild, +): WorkerRequestHandler { + const routes = createRoutes(build.routes); + + // If the core request handler is already wrapped, don't wrap Express handler which uses it. + if (isRequestHandlerWrapped) { + return origRequestHandler; + } + + return async function (this: unknown, request: Request): Promise { + if (!pkg) { + try { + pkg = await import('react-router-dom'); + } finally { + if (!pkg) { + __DEBUG_BUILD__ && logger.error('Could not find `react-router-dom` package.'); + } + } + } + + const hub = getCurrentHub(); + const options = hub.getClient()?.getOptions(); + const scope = hub.getScope(); + + scope.setSDKProcessingMetadata({ request }); + + if (!options || !hasTracingEnabled(options) || !request.url || !request.method) { + return origRequestHandler.call(this, request); + } + + const url = new URL(request.url); + const [name, source] = getTransactionName(routes, url, pkg); + startRequestHandlerTransaction(hub, name, source, { + headers: { + 'sentry-trace': + (request.headers && isString(request.headers.get('sentry-trace')) && request.headers.get('sentry-trace')) || + '', + baggage: (request.headers && isString(request.headers.get('baggage')) && request.headers.get('baggage')) || '', + }, + method: request.method, + }); + + return origRequestHandler.call(this, request); + }; +} + +/** + * Instruments `createRequestHandler` from `@remix-run/cloudflare-workers` + */ +export function wrapCloudflareWorkerCreateRequestHandler( + origCreateRequestHandler: CloudflareWorkerCreateRequestHandler, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (build: ServerBuild, mode?: string) => WorkerRequestHandler { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (this: unknown, build: ServerBuild, mode?: string): WorkerRequestHandler { + const newBuild = instrumentBuild(build); + const requestHandler = origCreateRequestHandler.call(this, newBuild, mode); + + return wrapCloudflareWorkerRequestHandler(requestHandler, newBuild); + }; +} diff --git a/packages/remix/src/worker/transport.ts b/packages/remix/src/worker/transport.ts index a479425f96e6..0043be033c63 100644 --- a/packages/remix/src/worker/transport.ts +++ b/packages/remix/src/worker/transport.ts @@ -1,94 +1,24 @@ import { createTransport } from '@sentry/core'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; -import { SentryError } from '@sentry/utils'; -export interface VercelEdgeTransportOptions extends BaseTransportOptions { - /** Fetch API init parameters. */ - fetchOptions?: RequestInit; - /** Custom headers for the transport. */ - headers?: { [key: string]: string }; -} - -const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; - -/** - * This is a modified promise buffer that collects tasks until drain is called. - * We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests - * and responses, and the normal PromiseBuffer inherently buffers stuff inbetween incoming requests. - * - * A limitation we need to be aware of is that DEFAULT_TRANSPORT_BUFFER_SIZE is the maximum amount of payloads the - * SDK can send for a given edge function invocation. - */ -export class IsolatedPromiseBuffer { - // We just have this field because the promise buffer interface requires it. - // If we ever remove it from the interface we should also remove it here. - public $: Array>; - - private _taskProducers: (() => PromiseLike)[]; - - private readonly _bufferSize: number; - - public constructor(_bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE) { - this.$ = []; - this._taskProducers = []; - this._bufferSize = _bufferSize; - } - - /** - * @inheritdoc - */ - public add(taskProducer: () => PromiseLike): PromiseLike { - if (this._taskProducers.length >= this._bufferSize) { - return Promise.reject(new SentryError('Not adding Promise because buffer limit was reached.')); - } - - this._taskProducers.push(taskProducer); - return Promise.resolve(); - } - - /** - * @inheritdoc - */ - public drain(timeout?: number): PromiseLike { - const oldTaskProducers = [...this._taskProducers]; - this._taskProducers = []; - - return new Promise(resolve => { - const timer = setTimeout(() => { - if (timeout && timeout > 0) { - resolve(false); - } - }, timeout); - - void Promise.all( - oldTaskProducers.map(taskProducer => - taskProducer().then(null, () => { - // catch all failed requests - }), - ), - ).then(() => { - // resolve to true if all fetch requests settled - clearTimeout(timer); - resolve(true); - }); - }); - } -} +export type CloudflareWorkersTransportOptions = BaseTransportOptions & { + headers?: Record; + context?: Record; + fetcher?: typeof fetch; +}; /** - * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. + * Creates a Transport that uses the Cloudflare Workers' fetch API to send events to Sentry. */ -export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transport { +export function makeCloudflareTransport(options: CloudflareWorkersTransportOptions): Transport { function makeRequest(request: TransportRequest): PromiseLike { const requestOptions: RequestInit = { body: request.body, method: 'POST', - referrerPolicy: 'origin', headers: options.headers, - ...options.fetchOptions, }; - return fetch(options.url, requestOptions).then(response => { + const fetchRequest = fetch(options.url, requestOptions).then(response => { return { statusCode: response.status, headers: { @@ -97,7 +27,13 @@ export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transpor }, }; }); + + if (options.context && options.context.waitUntil) { + options.context.waitUntil(fetchRequest); + } + + return fetchRequest; } - return createTransport(options, makeRequest, new IsolatedPromiseBuffer(options.bufferSize)); + return createTransport(options, makeRequest); } From e48663c80122110d260e9e6d0da286b0d59eaaea Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 13 Oct 2023 01:43:11 +0100 Subject: [PATCH 04/13] Add remix-cloudflare-workers e2e-test app. --- .github/workflows/build.yml | 1 + .../remix-cloudflare-workers/.dev.vars | 0 .../remix-cloudflare-workers/.eslintrc.cjs | 4 + .../remix-cloudflare-workers/.gitignore | 8 + .../remix-cloudflare-workers/.npmrc | 2 + .../app/entry.client.tsx | 49 +++++ .../app/entry.server.tsx | 50 +++++ .../remix-cloudflare-workers/app/root.tsx | 43 ++++ .../app/routes/_index.tsx | 21 ++ .../app/routes/client-error.tsx | 27 +++ .../app/routes/navigate.tsx | 20 ++ .../app/routes/server-error.tsx | 19 ++ .../app/routes/user.$id.tsx | 3 + .../remix-cloudflare-workers/createEnvFile.sh | 11 + .../remix-cloudflare-workers/globals.d.ts | 7 + .../remix-cloudflare-workers/package.json | 40 ++++ .../playwright.config.ts | 57 +++++ .../public/favicon.ico | Bin 0 -> 16958 bytes .../remix-cloudflare-workers/remix.config.js | 18 ++ .../remix-cloudflare-workers/remix.env.d.ts | 8 + .../remix-cloudflare-workers/server.ts | 54 +++++ .../tests/behaviour-client.test.ts | 208 ++++++++++++++++++ .../tests/behaviour-server.test.ts | 44 ++++ .../remix-cloudflare-workers/tsconfig.json | 22 ++ .../remix-cloudflare-workers/wrangler.toml | 12 + packages/remix/src/index.client.tsx | 8 +- .../remix/src/utils/serverAdapters/worker.ts | 2 +- 27 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/.dev.vars create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/.eslintrc.cjs create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/.gitignore create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/.npmrc create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.client.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.server.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/root.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/routes/_index.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/routes/client-error.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/routes/navigate.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/routes/server-error.tsx create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/app/routes/user.$id.tsx create mode 100755 packages/e2e-tests/test-applications/remix-cloudflare-workers/createEnvFile.sh create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/globals.d.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/package.json create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/public/favicon.ico create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/remix.config.js create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/remix.env.d.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/server.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/tests/behaviour-client.test.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/tests/behaviour-server.test.ts create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/tsconfig.json create mode 100644 packages/e2e-tests/test-applications/remix-cloudflare-workers/wrangler.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 690e076ce309..ad42927ee014 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -871,6 +871,7 @@ jobs: 'sveltekit', 'generic-ts3.8', 'node-experimental-fastify-app', + 'remix-cloudflare-workers', ] build-command: - false diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/.dev.vars b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.dev.vars new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/.eslintrc.cjs b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.eslintrc.cjs new file mode 100644 index 000000000000..f2faf1470fd8 --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.eslintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'], +}; diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/.gitignore b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.gitignore new file mode 100644 index 000000000000..0a2591659ac2 --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.gitignore @@ -0,0 +1,8 @@ +node_modules + +/.cache +/.wrangler +/build +/public/build +.env +env.ts diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/.npmrc b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.client.tsx b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.client.tsx new file mode 100644 index 000000000000..605d8e792d23 --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.client.tsx @@ -0,0 +1,49 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import { startTransition, StrictMode, useEffect } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + dsn: window.ENV.SENTRY_DSN, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.remixRouterInstrumentation(useEffect, useLocation, useMatches), + }), + new Sentry.Replay(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. +}); + +Sentry.addGlobalEventProcessor(event => { + if ( + event.type === 'transaction' && + (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') + ) { + const eventId = event.event_id; + if (eventId) { + window.recordedTransactions = window.recordedTransactions || []; + window.recordedTransactions.push(eventId); + } + } + + return event; +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.server.tsx b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.server.tsx new file mode 100644 index 000000000000..c2348037ce8d --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/entry.server.tsx @@ -0,0 +1,50 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import type { AppLoadContext, EntryContext, DataFunctionArgs } from '@remix-run/cloudflare'; +import { RemixServer } from '@remix-run/react'; +import isbot from 'isbot'; +import { renderToReadableStream } from 'react-dom/server'; +import * as Sentry from '@sentry/remix'; +import { env } from '../env'; + +Sentry.workerInit({ + dsn: env.SENTRY_DSN, + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + debug: true, +}); + +export function handleError(error: unknown, { request }: DataFunctionArgs): void { + Sentry.captureRemixServerException(error, 'remix.server', request); +} + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext, +) { + const body = await renderToReadableStream(, { + signal: request.signal, + onError(error: unknown) { + // Log streaming rendering errors from inside the shell + console.error(error); + responseStatusCode = 500; + }, + }); + + if (isbot(request.headers.get('user-agent'))) { + await body.allReady; + } + + responseHeaders.set('Content-Type', 'text/html'); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/root.tsx b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/root.tsx new file mode 100644 index 000000000000..b646820d6b9c --- /dev/null +++ b/packages/e2e-tests/test-applications/remix-cloudflare-workers/app/root.tsx @@ -0,0 +1,43 @@ +import { json, LinksFunction } from '@remix-run/cloudflare'; +import { cssBundleHref } from '@remix-run/css-bundle'; +import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; +import { env } from '../env'; + +export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])]; + +export const loader = () => { + return json({ + ENV: { + SENTRY_DSN: env.SENTRY_DSN, + }, + }); +}; + +function App() { + const { ENV } = useLoaderData(); + + return ( + + + + +