diff --git a/packages/urql/README.md b/packages/urql/README.md new file mode 100644 index 00000000..1e3c1b62 --- /dev/null +++ b/packages/urql/README.md @@ -0,0 +1,115 @@ +# `@appsignal/urql` + +- [AppSignal.com website][appsignal] +- [Documentation][docs] +- [Support][contact] + +The `@appsignal/javascript` integration for urql GraphQL client. + +See also the [mono repo README](../../README.md) for more information. + +## Installation + +Add the `@appsignal/urql` and `@appsignal/javascript` packages to your `package.json`. Then, run `yarn install`/`npm install`. + +You can also add these packages to your `package.json` on the command line: + +``` +yarn add @appsignal/javascript @appsignal/urql urql wonka +npm install --save @appsignal/javascript @appsignal/urql urql wonka +``` + +## Usage + +### Urql Exchange + +The `@appsignal/urql` package provides a custom urql exchange that automatically reports GraphQL errors to AppSignal. This exchange intercepts all query and mutation results and reports any errors without requiring changes to individual `useQuery` calls. + +```typescript +import { createClient, fetchExchange } from 'urql'; +import Appsignal from '@appsignal/javascript'; +import { createAppsignalExchange } from '@appsignal/urql'; + +const appsignal = new Appsignal({ + key: 'YOUR FRONTEND API KEY' +}); + +const client = createClient({ + url: 'https://api.example.com/graphql', + exchanges: [createAppsignalExchange(appsignal), fetchExchange] +}); +``` + +The exchange will automatically: +- Report all GraphQL errors to AppSignal +- Include the GraphQL query body as a parameter (visible in AppSignal's error details) +- Include the endpoint URL as a tag +- Include operation name and type as tags (when available) + +### Error Details + +When a GraphQL error occurs, AppSignal will receive: + +- **Error message**: A concatenation of all GraphQL error messages +- **Tags**: + - `endpoint`: The GraphQL endpoint URL + - `operationName`: The name of the GraphQL operation (if specified) + - `operationType`: The type of operation (query, mutation, subscription) +- **Parameters**: + - `query`: The full GraphQL query body + +This provides complete context for debugging GraphQL errors in your application. + +## Development + +### Installation + +Make sure mono is installed and bootstrapped, see the [project README's development section](../../README.md#dev-install) for more information. + +You can then run the following to start the compiler in _watch_ mode. This automatically compiles both the ES Module and CommonJS variants: + +```bash +yarn build:watch +``` + +You can also build the library without watching the directory: + +``` +yarn build # build both CJS and ESM +yarn build:cjs # just CJS +yarn build:esm # just ESM +``` + +### Testing + +The tests for this library use [Jest](https://jestjs.io) as the test runner. Once you've installed the dependencies, you can run the following command in the root of this repository to run the tests for all packages, or in the directory of a package to run only the tests pertaining to that package: + +```bash +yarn test +``` + +### Versioning + +This repo uses [Semantic Versioning][semver] (often referred to as _semver_). Each package in the repository is versioned independently from one another. + +## Contributing + +Thinking of contributing to this repo? Awesome! 🚀 + +Please follow our [Contributing guide][contributing-guide] in our documentation and follow our [Code of Conduct][coc]. + +Also, we would be very happy to send you Stroopwafles. Have look at everyone we send a package to so far on our [Stroopwafles page][waffles-page]. + +## Support + +[Contact us][contact] and speak directly with the engineers working on AppSignal. They will help you get set up, tweak your code and make sure you get the most out of using AppSignal. + +[appsignal]: https://appsignal.com +[appsignal-sign-up]: https://appsignal.com/users/sign_up +[contact]: mailto:support@appsignal.com +[coc]: https://docs.appsignal.com/appsignal/code-of-conduct.html +[waffles-page]: https://appsignal.com/waffles +[docs]: http://docs.appsignal.com +[contributing-guide]: http://docs.appsignal.com/appsignal/contributing.html + +[semver]: http://semver.org/ diff --git a/packages/urql/jest.config.js b/packages/urql/jest.config.js new file mode 100644 index 00000000..c6644a88 --- /dev/null +++ b/packages/urql/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "jsdom", + roots: ["/src"], + transform: { + "^.+\\.tsx?$": "ts-jest" + } +} diff --git a/packages/urql/package.json b/packages/urql/package.json new file mode 100644 index 00000000..bbbd2840 --- /dev/null +++ b/packages/urql/package.json @@ -0,0 +1,34 @@ +{ + "name": "@appsignal/urql", + "version": "1.0.0", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "repository": { + "type": "git", + "url": "https://github.com/appsignal/appsignal-javascript.git", + "directory": "packages/urql" + }, + "license": "MIT", + "scripts": { + "build": "npm run build:cjs && npm run build:esm", + "build:esm": "tsc -p tsconfig.esm.json", + "build:esm:watch": "tsc -p tsconfig.esm.json -w --preserveWatchOutput", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:cjs:watch": "tsc -p tsconfig.cjs.json -w --preserveWatchOutput", + "build:watch": "run-p build:cjs:watch build:esm:watch", + "clean": "rimraf dist coverage", + "link:npm": "npm link", + "test": "jest --passWithNoTests", + "test:watch": "jest --watch" + }, + "dependencies": { + "@appsignal/javascript": "=1.6.1" + }, + "peerDependencies": { + "urql": ">= 2.0.0 < 5", + "wonka": ">= 4.0.0 < 7" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/urql/src/index.ts b/packages/urql/src/index.ts new file mode 100644 index 00000000..6bbf1c66 --- /dev/null +++ b/packages/urql/src/index.ts @@ -0,0 +1,68 @@ +import { pipe, tap } from 'wonka'; +import type Appsignal from '@appsignal/javascript'; + +/** + * Custom urql exchange that automatically reports GraphQL errors to AppSignal. + * + * This exchange intercepts all query/mutation results and reports any errors + * to AppSignal without requiring changes to individual useQuery calls. + * + * @example + * ```typescript + * import { createClient, fetchExchange } from 'urql'; + * import Appsignal from '@appsignal/javascript'; + * import { createAppsignalExchange } from '@appsignal/urql'; + * + * const appsignal = new Appsignal({ + * key: 'YOUR FRONTEND API KEY' + * }); + * + * const client = createClient({ + * url: 'https://api.example.com/graphql', + * exchanges: [createAppsignalExchange(appsignal), fetchExchange] + * }); + * ``` + */ +export const createAppsignalExchange = (appsignal: Appsignal) => ({ forward, client }: any) => (ops$: any) => { + return pipe( + forward(ops$), + tap((result: any) => { + if (result.error) { + const { error, operation } = result; + + // Convert CombinedError to a proper Error with meaningful message + const errorMessage = error.graphQLErrors?.length > 0 + ? error.graphQLErrors.map((e: any) => e.message).join(', ') + : error.message; + + const reportError = new Error(`GraphQL Error: ${errorMessage}`); + reportError.name = 'GraphQLError'; + (reportError as any).stack = error.stack || reportError.stack; + + // Send error to AppSignal with metadata + appsignal.sendError(reportError, (span) => { + // Add endpoint URL as a tag + if (client?.url) { + span.setTags({ endpoint: client.url }); + } + + // Add GraphQL query body as a param + if (operation?.query) { + const queryBody = operation.query.loc?.source?.body; + if (queryBody) { + span.setParams({ query: queryBody }); + } + } + + // Add operation metadata + if (operation?.operationName) { + span.setTags({ operationName: operation.operationName }); + } + if (operation?.kind) { + span.setTags({ operationType: operation.kind }); + } + }); + } + }) + ); +}; diff --git a/packages/urql/tsconfig.cjs.json b/packages/urql/tsconfig.cjs.json new file mode 100644 index 00000000..ac31cb33 --- /dev/null +++ b/packages/urql/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/cjs", + "module": "commonjs" + } +} diff --git a/packages/urql/tsconfig.esm.json b/packages/urql/tsconfig.esm.json new file mode 100644 index 00000000..b3f357a2 --- /dev/null +++ b/packages/urql/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "module": "es6" + } +} diff --git a/packages/urql/tsconfig.json b/packages/urql/tsconfig.json new file mode 100644 index 00000000..70d0a8f8 --- /dev/null +++ b/packages/urql/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"], + "exclude": [ + "src/**/__tests__", + "src/**/__mocks__" + ], + "compilerOptions": { + "rootDir": "./src", + "target": "es5" + } +}