├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode ├── launch.json └── settings.json ├── APACHEv2-LICENSE.md ├── README.md ├── circle.yml ├── package.json ├── scripts ├── clean.sh ├── compile.sh ├── deps.sh ├── include │ ├── node.sh │ └── shell.sh ├── release.sh ├── start.sh ├── test.sh ├── test:compile.sh ├── test:style.sh ├── test:unit.sh └── test:unit:coverage.sh ├── src └── index.ts ├── test ├── env │ ├── base.ts │ ├── integration.ts │ └── unit.ts ├── helpers │ ├── index.ts │ └── mocha.ts └── unit │ ├── index.ts │ └── jest.json ├── tsconfig.json ├── tslint.json └── typings ├── chai-jest-diff └── index.d.ts ├── global └── test │ ├── base.d.ts │ └── unit.d.ts └── jest └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | **/.DS_Store 3 | 4 | # Node 5 | /node_modules/ 6 | yarn.lock 7 | npm-shrinkwrap.json 8 | package-lock.json 9 | 10 | # Build Artifacts 11 | /output/ 12 | *.d.ts 13 | *.js 14 | !/scripts/*.js 15 | 16 | # TypeScript 17 | !/typings/**/*.d.ts 18 | 19 | # Coverage 20 | .nyc_output/ 21 | coverage/ 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.13.0 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "protocol": "legacy", 8 | "console": "integratedTerminal", 9 | "name": "test-unit", 10 | "runtimeExecutable": "${workspaceRoot}/scripts/test:unit.sh", 11 | "runtimeArgs": [ 12 | "--runInBand" 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Editor 3 | "editor.insertSpaces": true, 4 | "editor.tabSize": 2, 5 | "editor.rulers": [80, 120], 6 | 7 | // Files 8 | "files.insertFinalNewline": true, 9 | "files.trimTrailingWhitespace": true, 10 | "files.exclude": { 11 | "**/.DS_Store": true, 12 | "**/.git/**": true, 13 | "**/node_modules/**": true, 14 | "**/dist/**": true, 15 | "{src,test}/**/*.d.ts": true, 16 | "{src,test}/**/*.js": true, 17 | ".nyc_output/**": true, 18 | "coverage/**": true 19 | }, 20 | "files.watcherExclude": { 21 | "**/.DS_Store": true, 22 | "**/.git/objects/**": true, 23 | "**/node_modules/**": true, 24 | "**/dist/**": true, 25 | "{src,test}/**/*.d.ts": true, 26 | "{src,test}/**/*.js": true, 27 | ".nyc_output/**": true, 28 | "coverage/**": true 29 | }, 30 | 31 | // TypeScript 32 | "typescript.tsdk": "./node_modules/typescript/lib", 33 | 34 | // TSLint 35 | "tslint.autoFixOnSave": true, 36 | "tslint.ignoreDefinitionFiles": false 37 | } 38 | -------------------------------------------------------------------------------- /APACHEv2-LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License, Version 2.0 2 | 3 | Copyright 2017 Convoy, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 6 | this file except in compliance with the License. You may obtain a copy of the 7 | License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software distributed 12 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 14 | specific language governing permissions and limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apollo Link Tracer 2 | 3 | Trace your apollo queries and mutations with [apollo-link](https://github.com/apollographql/apollo-link). 4 | 5 | Relies on [@convoy/tracer](https://github.com/convoyinc/tracer). 6 | 7 | ## Getting started 8 | 9 | ``` 10 | npm install apollo-link-tracer --save 11 | ``` 12 | 13 | ```js 14 | import ApolloLinkTracer from 'apollo-link-tracer'; 15 | import { Reporter } from '@convoy/tracer'; 16 | 17 | const apiReporter = new Reporter({ 18 | flushHandler: async (timings, traces) => { 19 | // Report traces to API 20 | }, 21 | }); 22 | 23 | new ApolloClient({ 24 | link: ApolloLink.from([ 25 | new ApolloLinkTracer({ 26 | service: 'my-service', 27 | tracerConfig: { 28 | reporter: apiReporter, 29 | fullTraceSampleRate: 1, 30 | }, 31 | name: 'apollo-link', 32 | }), 33 | 34 | new ApolloLink((operation, forward) => { 35 | const { tracer } = operation.getContext().tracer; 36 | const requestId = tracer.getTraceId(); 37 | 38 | operation.setContext({ 39 | headers: { 40 | 'x-req-id': requestId, 41 | }, 42 | }); 43 | 44 | return forward(operation); 45 | }), 46 | 47 | new RetryLink(retryOptions), 48 | 49 | new ApolloLinkTracer({ 50 | service: 'my-service', 51 | tracerConfig: { 52 | reporter: apiReporter, 53 | fullTraceSampleRate: 1, 54 | }, 55 | name: 'apollo-link-retry', 56 | }), 57 | 58 | new HttpLink({ uri, fetch }), 59 | ]), 60 | }); 61 | ``` 62 | 63 | Applying apollo-link-tracer both before and after the retry link lets you separately measure each retry, but provides a single trace across all of them. 64 | 65 | A Tracer object is added to the operation context, which can then be used to pass a trace id to the HTTP headers to support cross service tracing. 66 | 67 | ### Advanced options 68 | 69 | On a per GQL request basis you can alter how the trace is captured. 70 | 71 | ```js 72 | api.mutate({ 73 | mutation, 74 | variables, 75 | context: { 76 | tracerConfig: { 77 | fullTraceSampleRate: 1 / 20, 78 | }, 79 | traceService: 'my-service', 80 | traceName: 'apollo-link-mutate', 81 | traceResource: 'getSomething', 82 | avoidTrace: false, // If true, skips tracing 83 | }, 84 | }); 85 | ``` 86 | 87 | #### Filtering Sensitive Variables 88 | 89 | To avoid reporting the contents of sensitive variables, you can specify variable filters. 90 | 91 | ```js 92 | new ApolloLinkTracer({ 93 | service: 'my-service', 94 | tracerConfig: { 95 | reporter: apiReporter, 96 | fullTraceSampleRate: 1, 97 | }, 98 | name: 'apollo-link-retry', 99 | variableFilters: ['password', /secret.*sauce/], 100 | }), 101 | ``` 102 | 103 | Variable filters can be either string matches or regular expressions. Any variables whose names match these filters will have their values filtered out, e.g. 104 | 105 | ```js 106 | { username: 'bob', password: 'hunter2', secret_hot_sauce: 'sri racha'} 107 | ``` 108 | 109 | becomes 110 | 111 | ```js 112 | { username: 'bob', password: '', secret_hot_sauce: ''} 113 | ``` 114 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | node: 5 | version: 10.13.0 6 | environment: 7 | DOCKER_CACHE_DIR: /home/ubuntu/docker_cache 8 | 9 | dependencies: 10 | cache_directories: 11 | - ~/.cache/yarn 12 | override: 13 | - yarn config set registry https://registry.npmjs.org/ 14 | - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 15 | - nvm install && nvm alias default $(cat .nvmrc) 16 | - yarn 17 | 18 | test: 19 | # Rather than just running `npm test` (which would be Circle's default) 20 | # we run explicit steps so that a build failure has a clearer source. 21 | # 22 | # Note that most tests are configured to run in parallel, so that you can get 23 | # immediate gains by configuring # of containers via Circle. 24 | override: 25 | - yarn run test:compile 26 | 27 | - yarn run test:style: 28 | parallel: true 29 | files: 30 | - src/**/*.{ts,tsx} 31 | - test/**/*.{ts,tsx} 32 | 33 | - yarn run test:unit: 34 | parallel: true 35 | files: 36 | - test/unit/**/*.{ts,tsx} 37 | environment: 38 | MOCHA_FILE: "$CIRCLE_TEST_REPORTS/test:unit.xml" 39 | 40 | deployment: 41 | deploy: 42 | branch: master 43 | commands: 44 | - git config --global user.email "donvoy@convoy.com" 45 | - git config --global user.name "Don Voy" 46 | - yarn run release 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-link-tracer", 3 | "version": "1.0.0", 4 | "description": "Trace your apollo queries and mutations", 5 | "license": "Apache-2.0", 6 | "files": ["*.md", "src/**.js", "src/**.d.ts"], 7 | "main": "./src/index.js", 8 | "typings": "./src/index.d.ts", 9 | "repository": "convoyinc/apollo-link-tracer", 10 | "scripts": { 11 | "clean": "./scripts/clean.sh", 12 | "deps": "./scripts/deps.sh", 13 | "dev": "./scripts/dev.sh", 14 | "compile": "./scripts/compile.sh", 15 | "release": "./scripts/release.sh", 16 | "start": "./scripts/start.sh", 17 | "test": "./scripts/test.sh", 18 | "test:compile": "./scripts/test:compile.sh", 19 | "test:style": "./scripts/test:style.sh", 20 | "test:unit": "./scripts/test:unit.sh", 21 | "precommit": "lint-staged" 22 | }, 23 | "lint-staged": { 24 | "*.{js,json,md,ts}": [ 25 | "prettier --single-quote --trailing-comma all --write", 26 | "git add" 27 | ] 28 | }, 29 | "dependencies": { 30 | "@convoy/tracer": "^1.0.144", 31 | "apollo-link": "^1.1.0", 32 | "tslib": "^1.6.0" 33 | }, 34 | "devDependencies": { 35 | "@types/chai": "^3.5.2", 36 | "@types/chai-as-promised": "^0.0.30", 37 | "@types/graphql": "^0.12.4", 38 | "@types/lodash": "^4.14.104", 39 | "@types/node": "^6.0.52", 40 | "@types/sinon": "^2.3.4", 41 | "@types/sinon-chai": "^2.7.29", 42 | "chai": "^3.5.0", 43 | "chai-as-promised": "^6.0.0", 44 | "chai-jest-diff": "nevir/chai-jest-diff#built-member-assertions", 45 | "graphql": "^0.13.1", 46 | "graphql-tag": "^2.8.0", 47 | "husky": "^0.14.3", 48 | "jest": "^21.1.0", 49 | "jest-junit": "^3.0.0", 50 | "lint-staged": "^6.0.1", 51 | "prettier": "^1.10.2", 52 | "sinon": "^3.2.1", 53 | "sinon-chai": "^2.13.0", 54 | "tslint": "^5.8.0", 55 | "tslint-no-unused-expression-chai": "0.0.3", 56 | "typescript": "^2.7.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | FILES_TO_REMOVE=($( 5 | find . \ 6 | \( -name "*.d.ts" -or -name "*.js" -or -name "*.js.map" \) \ 7 | -not -path "./scripts/*" \ 8 | -not -path "./coverage/*" \ 9 | -not -path "./node_modules/*" \ 10 | -not -path "./typings/*" 11 | )) 12 | 13 | if [[ "${#FILES_TO_REMOVE[@]}" != "0" ]]; then 14 | echo 15 | for file in "${FILES_TO_REMOVE[@]}"; do 16 | echo " ${file}" 17 | rm "${file}" 18 | done 19 | echo 20 | fi 21 | 22 | # We also just drop some trees completely. 23 | rm -rf ./output 24 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | yarn run clean 7 | tsc "${@}" 8 | -------------------------------------------------------------------------------- /scripts/deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | announce() { >&2 echo $1; } 5 | 6 | # Use relaunch to bring old docker containers to life. If it 7 | # returns a non-zero exit code, you'll have to run the docker 8 | # container yourself. 9 | # E.g. 10 | # 11 | # if [[ "$(relaunch $DOCKER_CONTAINER_NAME)" -ne "0" ]]; then 12 | # docker run -d --name $DOCKER_CONTAINER_NAME $DOCKER_IMAGE; 13 | # fi 14 | relaunch() { 15 | local name=$1 16 | local ALIVE=`docker ps -f name=$name -f status=running -q` 17 | local STOPPED=`docker ps -f name=$name -f status=exited -q` 18 | if [[ -n $STOPPED ]]; then 19 | announce "Restarting stopped $name container ID $STOPPED ('docker rm $STOPPED' to force a fresh copy)" 20 | docker start $STOPPED > /dev/null 2>&1 21 | echo 0 22 | elif [[ -n $ALIVE ]]; then 23 | announce "$name container is running, ID $ALIVE" 24 | echo 0 25 | else 26 | # could not relaunch 27 | echo 1 28 | fi 29 | } 30 | 31 | announce "All done" 32 | -------------------------------------------------------------------------------- /scripts/include/node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (( __NODE_INCLUDED__ )) && return 4 | __NODE_INCLUDED__=1 5 | 6 | if [[ ! -f ./.nvmrc ]]; then 7 | echo ".nvmrc is missing from the repo root (or paths are screwed up?)" 1>&2 8 | exit 1 9 | fi 10 | 11 | CURRENT_NODE_VERSION=$(node --version 2> /dev/null || echo 'none') 12 | DESIRED_NODE_VERSION=v$(cat ./.nvmrc) 13 | if [[ "${CURRENT_NODE_VERSION}" != "${DESIRED_NODE_VERSION}" ]]; then 14 | # Attempt to switch to the correct node for this shell… 15 | if ! type nvm >/dev/null 2>&1 && [[ -x "${NVM_DIR}"/nvm.sh ]]; then 16 | source "${NVM_DIR}"/nvm.sh 17 | fi 18 | if type nvm > /dev/null 2>&1; then 19 | nvm use 20 | fi 21 | 22 | CURRENT_NODE_VERSION=$(node --version 2>/dev/null || echo 'none') 23 | if [[ "${CURRENT_NODE_VERSION}" != "${DESIRED_NODE_VERSION}" ]]; then 24 | echo "Wrong node version. Found ${CURRENT_NODE_VERSION}, expected ${DESIRED_NODE_VERSION}…" 1>&2 25 | echo "…and was unable to switch to ${DESIRED_NODE_VERSION} via nvm." 1>&2 26 | exit 1 27 | fi 28 | fi 29 | 30 | # Note that VS Code gets a free pass 31 | if [[ ! "${npm_config_user_agent}" =~ yarn/ && "${VSCODE_PID}" == "" ]]; then 32 | echo "Please use yarn to run scripts in this repository." 1>&2 33 | exit 1 34 | fi 35 | 36 | export PATH=$(yarn bin):$PATH 37 | 38 | current_yarn_command() { 39 | node -e "console.log(JSON.parse(process.env.npm_config_argv).cooked[0])" 40 | } 41 | 42 | write_package_key() { 43 | local KEY="${1}" 44 | local VALUE="${2}" 45 | 46 | node <<-end_script 47 | const _ = require('lodash'); 48 | const fs = require('fs'); 49 | 50 | const packageInfo = JSON.parse(fs.readFileSync('package.json')); 51 | _.set(packageInfo, '${KEY}', '${VALUE}'); 52 | fs.writeFileSync('package.json', JSON.stringify(packageInfo, null, 2)); 53 | end_script 54 | } 55 | -------------------------------------------------------------------------------- /scripts/include/shell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (( __SHELL_INCLUDED__ )) && return 4 | __SHELL_INCLUDED__=1 5 | 6 | export OPTIONS_FLAGS=() 7 | export OPTIONS_ARGS=() 8 | for argument in "${@}"; do 9 | if [[ "${argument}" =~ ^- ]]; then 10 | OPTIONS_FLAGS+=("${argument}") 11 | else 12 | OPTIONS_ARGS+=("${argument}") 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | PACKAGE_XY=$(node -e "console.log(JSON.parse(fs.readFileSync('package.json')).version.replace(/\.\d+$/, ''))") 7 | PACKAGE_VERSION="${PACKAGE_XY}.${CIRCLE_BUILD_NUM}" 8 | 9 | yarn compile 10 | 11 | echo "Releasing ${PACKAGE_VERSION}" 12 | write_package_key version "${PACKAGE_VERSION}" 13 | git add package.json 14 | git commit -m "v${PACKAGE_VERSION}" 15 | git tag v${PACKAGE_VERSION} 16 | 17 | git push --tags 18 | yarn publish --new-version $PACKAGE_VERSION 19 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | # Kill all child processes on exit. 7 | trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT 8 | 9 | # Compile TypeScript sources in the background; but wait for it to have 10 | # completed the first compile run before passing control off to Jest. 11 | 12 | tsc_fifo=$(mktemp -u) 13 | mkfifo "${tsc_fifo}" 14 | 15 | yarn run compile --watch >"${tsc_fifo}" 2>&1 & 16 | while read -r line; do 17 | echo "${line}" 18 | if [[ "${line}" =~ "Compilation complete" ]]; then 19 | break 20 | fi 21 | done <"${tsc_fifo}" 22 | # Finally, redirect tsc back to stdout 23 | cat "${tsc_fifo}" >&1 & 24 | 25 | # Let jest own our process & stdin. 26 | yarn run test:unit --watch 27 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | yarn run test:compile 5 | yarn run test:style 6 | yarn run test:unit 7 | -------------------------------------------------------------------------------- /scripts/test:compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | tsc --noEmit 7 | -------------------------------------------------------------------------------- /scripts/test:style.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | FILES=("${@}") 7 | if [[ "${#FILES[@]}" = "0" ]]; then 8 | FILES+=($(find scripts src test ! -name "*.d.ts" -and -name "*.ts" -or -name "*.tsx")) 9 | fi 10 | 11 | OPTIONS=( 12 | --format codeFrame 13 | --project tsconfig.json 14 | ) 15 | 16 | set -x 17 | tslint "${OPTIONS[@]}" "${FILES[@]}" 18 | -------------------------------------------------------------------------------- /scripts/test:unit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/shell.sh 5 | source ./scripts/include/node.sh 6 | 7 | FILES=("${OPTIONS_ARGS[@]}") 8 | if [[ "${#FILES[@]}" == "0" ]]; then 9 | FILES+=($( 10 | find ./test/unit \ 11 | \( -name "*.ts" -not -name "*.d.ts" \) \ 12 | -or -name "*.tsx" 13 | )) 14 | fi 15 | 16 | # We take ts files as arguments for everyone's sanity; but redirect to their 17 | # compiled sources under the covers. 18 | for i in "${!FILES[@]}"; do 19 | file="${FILES[$i]}" 20 | if [[ "${file##*.}" == "ts" ]]; then 21 | FILES[$i]="${file%.*}.js" 22 | fi 23 | done 24 | 25 | OPTIONS=( 26 | --config ./test/unit/jest.json 27 | ) 28 | # Jest doesn't handle debugger flags directly. 29 | NODE_OPTIONS=() 30 | for option in "${OPTIONS_FLAGS[@]}"; do 31 | if [[ "${option}" =~ ^--(inspect|debug-brk|nolazy) ]]; then 32 | NODE_OPTIONS+=("${option}") 33 | else 34 | OPTIONS+=("${option}") 35 | fi 36 | done 37 | 38 | yarn run compile 39 | 40 | # For jest-junit 41 | export JEST_SUITE_NAME="test:unit" 42 | export JEST_JUNIT_OUTPUT=./output/test:unit/report.xml 43 | 44 | node "${NODE_OPTIONS[@]}" ./node_modules/.bin/jest "${OPTIONS[@]}" "${FILES[@]}" 45 | -------------------------------------------------------------------------------- /scripts/test:unit:coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | OPTIONS=() 7 | if [[ "${CI}" == "" ]]; then 8 | OPTIONS+=( 9 | --coverageReporters html 10 | ) 11 | fi 12 | 13 | yarn run test:unit --coverage "${OPTIONS[@]}" 14 | 15 | if [[ "${CI}" == "" ]]; then 16 | open ./output/test:unit/index.html 17 | else 18 | codecov --file=./output/test:unit/lcov.info 19 | fi 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloLink, 3 | FetchResult, 4 | NextLink, 5 | Observable, 6 | Operation, 7 | } from 'apollo-link'; 8 | import { 9 | AnnotatorFunction, 10 | SpanMeta, 11 | Span, 12 | SpanTags, 13 | Tracer, 14 | TracerConfiguration, 15 | } from '@convoy/tracer'; 16 | 17 | export type ErrorAnnotatorFunction = (span: Span, error: Error) => void; 18 | export type GraphQLErrorAnnotatorFunction = ( 19 | span: Span, 20 | result: FetchResult, 21 | ) => void; 22 | export type VariableFilter = string | RegExp; 23 | 24 | export default class ApolloLinkTracer extends ApolloLink { 25 | private service: string; 26 | private tracerConfig: TracerConfiguration; 27 | private name?: string; 28 | private annotator?: AnnotatorFunction; 29 | private metadata?: SpanMeta; 30 | private tags?: SpanTags; 31 | private networkErrorAnnotator: ErrorAnnotatorFunction; 32 | private graphQLErrorAnnotator: GraphQLErrorAnnotatorFunction; 33 | private variableFilters: VariableFilter[]; 34 | 35 | constructor({ 36 | service, 37 | tracerConfig, 38 | name, 39 | annotator, 40 | metadata, 41 | tags, 42 | networkErrorAnnotator = baseErrorAnnotator, 43 | graphQLErrorAnnotator = baseGQLAnnotator, 44 | variableFilters = [], 45 | }: { 46 | service: string; 47 | tracerConfig: TracerConfiguration; 48 | name?: string; 49 | annotator?: AnnotatorFunction; 50 | metadata?: SpanMeta; 51 | tags?: SpanTags; 52 | networkErrorAnnotator?: ErrorAnnotatorFunction; 53 | graphQLErrorAnnotator?: GraphQLErrorAnnotatorFunction; 54 | variableFilters?: VariableFilter[]; 55 | }) { 56 | super(); 57 | this.service = service; 58 | this.tracerConfig = tracerConfig; 59 | this.name = name; 60 | this.metadata = metadata; 61 | this.tags = tags; 62 | this.annotator = annotator; 63 | this.networkErrorAnnotator = networkErrorAnnotator; 64 | this.graphQLErrorAnnotator = graphQLErrorAnnotator; 65 | this.variableFilters = variableFilters; 66 | } 67 | 68 | request(operation: Operation, forward: NextLink) { 69 | const context = operation.getContext(); 70 | 71 | if (context.avoidTrace) return forward(operation); 72 | 73 | const service = context.traceService || this.service; 74 | const name = context.traceName || this.name || 'apollo-link-tracer'; 75 | 76 | let tracer = context.tracer; 77 | 78 | let span: Span; 79 | if (tracer) { 80 | const resource = context.traceResource || tracer.get().resource; 81 | span = tracer.startNestedSpan(resource, name, service); 82 | } else { 83 | tracer = new Tracer({ 84 | ...this.tracerConfig, 85 | ...context.traceConfig, 86 | }); 87 | operation.setContext({ tracer }); 88 | const resource = context.traceResource || operation.operationName; 89 | span = tracer.start(resource, name, service); 90 | } 91 | span.setMeta({ 92 | ...context.traceMetadata, 93 | ...this.metadata, 94 | variables: variablesToMetaTag(operation.variables, this.variableFilters), 95 | }); 96 | span.setTags({ 97 | ...context.traceTags, 98 | ...this.tags, 99 | }); 100 | return new Observable(observer => { 101 | const sub = forward(operation).subscribe({ 102 | next: result => { 103 | if (result.errors) { 104 | this.graphQLErrorAnnotator(span, result); 105 | } 106 | if (tracer.get() === span) { 107 | tracer.end(); 108 | } else { 109 | span.end(); 110 | } 111 | const annotator = context.traceAnnotator || this.annotator; 112 | if (annotator) { 113 | annotator(span); 114 | } 115 | observer.next(result); 116 | }, 117 | error: networkError => { 118 | this.networkErrorAnnotator(span, networkError); 119 | if (tracer.get() === span) { 120 | tracer.end(); 121 | } else { 122 | span.end(); 123 | } 124 | observer.error(networkError); 125 | }, 126 | complete: observer.complete.bind(observer), 127 | }); 128 | 129 | return () => { 130 | sub.unsubscribe(); 131 | }; 132 | }); 133 | } 134 | } 135 | 136 | function variablesToMetaTag(variables: {}, variableFilters: VariableFilter[]) { 137 | const filteredVariables = { ...variables }; 138 | for (const varName of Object.keys(filteredVariables)) { 139 | for (const filter of variableFilters) { 140 | if ( 141 | (filter instanceof RegExp && filter.test(varName)) || 142 | (typeof filter === 'string' && filter === varName) 143 | ) { 144 | filteredVariables[varName] = ''; 145 | } 146 | } 147 | } 148 | return JSON.stringify(filteredVariables); 149 | } 150 | 151 | function baseErrorAnnotator(span: Span, error: Error) { 152 | span.setError(error); 153 | } 154 | 155 | export function baseGQLAnnotator(span: Span, result: FetchResult) { 156 | if (!result.errors) return; 157 | span.error = 1; 158 | span.setTags({ 159 | error: '1', 160 | [`error.name`]: result.errors[0].name, 161 | }); 162 | result.errors.forEach((error, index) => { 163 | span.setMeta({ 164 | [`error${index ? index + 1 : ''}.name`]: error.name, 165 | [`error${index ? index + 1 : ''}.message`]: error.message, 166 | [`error${index ? index + 1 : ''}.path`]: error.path 167 | ? error.path.join('') 168 | : '', 169 | }); 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /test/env/base.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as chaiAsPromised from 'chai-as-promised'; 3 | 4 | // Chai 5 | 6 | // We prefer Chai's `expect` interface. 7 | global.expect = chai.expect; 8 | // Give us all the info! 9 | chai.config.truncateThreshold = 0; 10 | 11 | // Promise-aware chai assertions (that return promises themselves): 12 | // 13 | // await expect(promise).to.be.rejectedWith(/error/i); 14 | // 15 | chai.use(chaiAsPromised); 16 | -------------------------------------------------------------------------------- /test/env/integration.ts: -------------------------------------------------------------------------------- 1 | import './base'; 2 | -------------------------------------------------------------------------------- /test/env/unit.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as sinon from 'sinon'; 3 | import * as sinonChai from 'sinon-chai'; 4 | 5 | import { withMocha } from '../helpers'; 6 | 7 | import './base'; 8 | 9 | // Chai 10 | 11 | // http://chaijs.com/plugins/sinon-chai 12 | // 13 | // Adds assertions for sinon spies. 14 | // 15 | // expect(aSpy).to.have.been.calledWith('abc', 123) 16 | // 17 | chai.use(sinonChai); 18 | 19 | // Test Environment 20 | 21 | withMocha(() => { 22 | 23 | beforeEach(() => { 24 | // Prefer accessing sinon via the `sandbox` global. 25 | global.sandbox = sinon.sandbox.create(); 26 | }); 27 | 28 | afterEach(() => { 29 | global.sandbox.restore(); 30 | delete global.sandbox; 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mocha'; 2 | -------------------------------------------------------------------------------- /test/helpers/mocha.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls `callback` once Mocha has loaded its environment. 3 | * 4 | * See https://github.com/mochajs/mocha/issues/764 5 | */ 6 | export function withMocha(callback: () => void): void { 7 | if ('beforeEach' in global) { 8 | callback(); 9 | return; 10 | } 11 | 12 | setImmediate(() => { 13 | withMocha(callback); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/unit/index.ts: -------------------------------------------------------------------------------- 1 | import { Reporter } from '@convoy/tracer'; 2 | import { ApolloLink, execute, GraphQLRequest, Observable } from 'apollo-link'; 3 | import gql from 'graphql-tag'; 4 | import { DocumentNode } from 'graphql'; 5 | import * as sinon from 'sinon'; 6 | 7 | import ApolloLinkTracer from '../../src'; 8 | 9 | function getOperationName(doc: DocumentNode): string | null { 10 | let res: string | null = null; 11 | doc.definitions.forEach(definition => { 12 | if (definition.kind === 'OperationDefinition' && definition.name) { 13 | res = definition.name.value; 14 | } 15 | }); 16 | return res; 17 | } 18 | 19 | describe(`ApolloLinkTracer`, () => { 20 | let reporter: Reporter; 21 | let tracer: ApolloLinkTracer; 22 | let stub: sinon.SinonStub; 23 | 24 | beforeEach(() => { 25 | stub = sinon.stub(); 26 | 27 | reporter = new Reporter({ 28 | flushHandler: stub, 29 | }); 30 | 31 | tracer = new ApolloLinkTracer({ 32 | service: 'my-service', 33 | tracerConfig: { 34 | sampleRate: 1, 35 | reporter, 36 | }, 37 | }); 38 | }); 39 | 40 | it(`successfully links the next link and completes trace`, done => { 41 | const document: DocumentNode = gql` 42 | query test1($x: String) { 43 | test(x: $x) 44 | } 45 | `; 46 | const variables1 = { x: 'Hello World' }; 47 | 48 | const request1: GraphQLRequest = { 49 | query: document, 50 | variables: variables1, 51 | operationName: getOperationName(document)!, 52 | }; 53 | 54 | let called = 0; 55 | const link = ApolloLink.from([ 56 | tracer, 57 | new ApolloLink(operation => { 58 | called += 1; 59 | return new Observable(observer => { 60 | observer.next({}); 61 | observer.complete(); 62 | }); 63 | }), 64 | ]); 65 | 66 | execute(link, request1).subscribe(async result => { 67 | expect(called).to.be.eq(1); 68 | await reporter.flushIfNeeded(); 69 | expect(stub).to.have.been.called; 70 | done(); 71 | return null; 72 | }); 73 | }); 74 | 75 | it(`doesnt do trace if avoidTrace specified`, done => { 76 | const document: DocumentNode = gql` 77 | query test1($x: String) { 78 | test(x: $x) 79 | } 80 | `; 81 | const variables1 = { x: 'Hello World' }; 82 | 83 | const request1: GraphQLRequest = { 84 | query: document, 85 | variables: variables1, 86 | operationName: getOperationName(document)!, 87 | context: { 88 | avoidTrace: true, 89 | }, 90 | }; 91 | 92 | let called = 0; 93 | const link = ApolloLink.from([ 94 | tracer, 95 | new ApolloLink(operation => { 96 | called += 1; 97 | return new Observable(observer => { 98 | observer.next({}); 99 | observer.complete(); 100 | }); 101 | }), 102 | ]); 103 | 104 | execute(link, request1).subscribe(async result => { 105 | expect(called).to.be.eq(1); 106 | await reporter.flushIfNeeded(); 107 | expect(stub).to.not.have.been.called; 108 | done(); 109 | return null; 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/unit/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "automock": false, 3 | "collectCoverageFrom": [ 4 | "src/**/*.js" 5 | ], 6 | "coverageDirectory": "output/test:unit", 7 | "coverageReporters": ["lcovonly", "text"], 8 | "coveragePathIgnorePatterns": [ 9 | "/node_modules/", 10 | "/test/" 11 | ], 12 | "mapCoverage": true, 13 | "rootDir": "../..", 14 | "setupTestFrameworkScriptFile": "./test/env/unit.js", 15 | "testMatch": ["/test/unit/**/*.js"], 16 | "testResultsProcessor": "./node_modules/jest-junit" 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "inlineSourceMap": true, 8 | "lib": ["dom", "es2015", "esnext.asynciterable", "scripthost"], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "newLine": "LF", 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "pretty": true, 17 | "stripInternal": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "target": "es5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["tslint-no-unused-expression-chai"], 3 | // https://palantir.github.io/tslint/rules/ 4 | "rules": { 5 | // TypeScript Specific 6 | 7 | // If the compiler is sure of the type of something, don't bother declaring 8 | // the type of it. 9 | "no-inferrable-types": true, 10 | 11 | // /// void; 3 | } 4 | -------------------------------------------------------------------------------- /typings/global/test/base.d.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | 3 | declare global { 4 | const expect: typeof chai.expect; 5 | 6 | namespace NodeJS { 7 | export interface Global { 8 | expect: typeof chai.expect; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/global/test/unit.d.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | declare global { 4 | const sandbox: sinon.SinonSandbox; 5 | 6 | namespace NodeJS { 7 | export interface Global { 8 | sandbox: sinon.SinonSandbox; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/jest/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // MODIFIED BY CONVOY: 3 | // 4 | // * Global `expect` was removed to make room for our global chai `expect`. 5 | // 6 | 7 | // Type definitions for Jest 20.0.5 8 | // Project: http://facebook.github.io/jest/ 9 | // Definitions by: Asana , Ivo Stratev , jwbay , Alexey Svetliakov , Alex Jover Morales , Allan Lukwago 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | // TypeScript Version: 2.1 12 | 13 | declare var beforeAll: jest.Lifecycle; 14 | declare var beforeEach: jest.Lifecycle; 15 | declare var afterAll: jest.Lifecycle; 16 | declare var afterEach: jest.Lifecycle; 17 | declare var describe: jest.Describe; 18 | declare var fdescribe: jest.Describe; 19 | declare var xdescribe: jest.Describe; 20 | declare var it: jest.It; 21 | declare var fit: jest.It; 22 | declare var xit: jest.It; 23 | declare var test: jest.It; 24 | declare var xtest: jest.It; 25 | 26 | interface NodeRequire { 27 | /** Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. */ 28 | requireActual(moduleName: string): any; 29 | /** Returns a mock module instead of the actual module, bypassing all checks on whether the module should be required normally or not. */ 30 | requireMock(moduleName: string): any; 31 | } 32 | 33 | declare namespace jest { 34 | /** Provides a way to add Jasmine-compatible matchers into your Jest context. */ 35 | function addMatchers(matchers: jasmine.CustomMatcherFactories): typeof jest; 36 | /** Disables automatic mocking in the module loader. */ 37 | function autoMockOff(): typeof jest; 38 | /** Enables automatic mocking in the module loader. */ 39 | function autoMockOn(): typeof jest; 40 | /** 41 | * @deprecated use resetAllMocks instead 42 | */ 43 | function clearAllMocks(): typeof jest; 44 | /** Clears the mock.calls and mock.instances properties of all mocks. Equivalent to calling .mockClear() on every mocked function. */ 45 | function resetAllMocks(): typeof jest; 46 | /** Removes any pending timers from the timer system. If any timers have been scheduled, they will be cleared and will never have the opportunity to execute in the future. */ 47 | function clearAllTimers(): typeof jest; 48 | /** Indicates that the module system should never return a mocked version of the specified module, including all of the specificied module's dependencies. */ 49 | function deepUnmock(moduleName: string): typeof jest; 50 | /** Disables automatic mocking in the module loader. */ 51 | function disableAutomock(): typeof jest; 52 | /** Mocks a module with an auto-mocked version when it is being required. */ 53 | function doMock(moduleName: string): typeof jest; 54 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 55 | function dontMock(moduleName: string): typeof jest; 56 | /** Enables automatic mocking in the module loader. */ 57 | function enableAutomock(): typeof jest; 58 | /** Creates a mock function. Optionally takes a mock implementation. */ 59 | function callback(implementation: (...args: any[]) => T): Mock; 60 | function callback(implementation?: Function): Mock; 61 | /** Generate a mock function. */ 62 | function fn(): Mock; 63 | /** Use the automatic mocking system to generate a mocked version of the given module. */ 64 | function genMockFromModule(moduleName: string): T; 65 | /** Returns whether the given function is a mock function. */ 66 | function isMockFunction(callback: any): callback is Mock; 67 | /** Mocks a module with an auto-mocked version when it is being required. */ 68 | function mock(moduleName: string, factory?: any, options?: MockOptions): typeof jest; 69 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 70 | function resetModuleRegistry(): typeof jest; 71 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 72 | function resetModules(): typeof jest; 73 | /** Exhausts tasks queued by setImmediate(). */ 74 | function runAllImmediates(): typeof jest; 75 | /** Exhausts the micro-task queue (usually interfaced in node via process.nextTick). */ 76 | function runAllTicks(): typeof jest; 77 | /** Exhausts the macro-task queue (i.e., all tasks queued by setTimeout() and setInterval()). */ 78 | function runAllTimers(): typeof jest; 79 | /** Executes only the macro-tasks that are currently pending (i.e., only the tasks that have been queued by setTimeout() or setInterval() up to this point). 80 | * If any of the currently pending macro-tasks schedule new macro-tasks, those new tasks will not be executed by this call. */ 81 | function runOnlyPendingTimers(): typeof jest; 82 | /** Executes only the macro task queue (i.e. all tasks queued by setTimeout() or setInterval() and setImmediate()). */ 83 | function runTimersToTime(msToRun: number): typeof jest; 84 | /** Explicitly supplies the mock object that the module system should return for the specified module. */ 85 | function setMock(moduleName: string, moduleExports: T): typeof jest; 86 | /** Creates a mock function similar to jest.callback but also tracks calls to object[methodName] */ 87 | function spyOn(object: T, method: M): SpyInstance; 88 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 89 | function unmock(moduleName: string): typeof jest; 90 | /** Instructs Jest to use fake versions of the standard timer functions. */ 91 | function useFakeTimers(): typeof jest; 92 | /** Instructs Jest to use the real versions of the standard timer functions. */ 93 | function useRealTimers(): typeof jest; 94 | 95 | interface MockOptions { 96 | virtual?: boolean; 97 | } 98 | 99 | interface EmptyFunction { 100 | (): void; 101 | } 102 | 103 | interface DoneCallback { 104 | (...args: any[]): any 105 | fail(error?: string | { message: string }): any; 106 | } 107 | 108 | interface ProvidesCallback { 109 | (callback: DoneCallback): any; 110 | } 111 | 112 | interface Lifecycle { 113 | (callback: ProvidesCallback): any; 114 | } 115 | 116 | /** Creates a test closure */ 117 | interface It { 118 | /** 119 | * Creates a test closure. 120 | */ 121 | (name: string, callback?: ProvidesCallback, timeout?: number): void; 122 | /** Only runs this test in the current file. */ 123 | only: It; 124 | skip: It; 125 | concurrent: It; 126 | } 127 | 128 | interface Describe { 129 | (name: string, callback: EmptyFunction): void 130 | only: Describe; 131 | skip: Describe; 132 | } 133 | 134 | interface MatcherUtils { 135 | readonly isNot: boolean; 136 | utils: { 137 | readonly EXPECTED_COLOR: string; 138 | readonly RECEIVED_COLOR: string; 139 | ensureActualIsNumber(actual: any, matcherName?: string): void; 140 | ensureExpectedIsNumber(actual: any, matcherName?: string): void; 141 | ensureNoExpected(actual: any, matcherName?: string): void; 142 | ensureNumbers(actual: any, expected: any, matcherName?: string): void; 143 | /** get the type of a value with handling of edge cases like `typeof []` and `typeof null` */ 144 | getType(value: any): string; 145 | matcherHint(matcherName: string, received?: string, expected?: string, options?: { secondArgument?: string, isDirectExpectCall?: boolean }): string; 146 | pluralize(word: string, count: number): string; 147 | printExpected(value: any): string; 148 | printReceived(value: any): string; 149 | printWithType(name: string, received: any, print: (value: any) => string): string; 150 | stringify(object: {}, maxDepth?: number): string; 151 | } 152 | } 153 | 154 | interface ExpectExtendMap { 155 | [key: string]: (this: MatcherUtils, received: any, actual: any) => { message: () => string, pass: boolean }; 156 | } 157 | 158 | interface SnapshotSerializerOptions { 159 | callToJSON?: boolean; 160 | edgeSpacing?: string; 161 | spacing?: string; 162 | escapeRegex?: boolean; 163 | highlight?: boolean; 164 | indent?: number; 165 | maxDepth?: number; 166 | min?: boolean; 167 | plugins?: Array 168 | printFunctionName?: boolean; 169 | theme?: SnapshotSerializerOptionsTheme; 170 | 171 | // see https://github.com/facebook/jest/blob/e56103cf142d2e87542ddfb6bd892bcee262c0e6/types/PrettyFormat.js 172 | } 173 | interface SnapshotSerializerOptionsTheme { 174 | comment?: string; 175 | content?: string; 176 | prop?: string; 177 | tag?: string; 178 | value?: string; 179 | } 180 | interface SnapshotSerializerColor { 181 | close: string; 182 | open: string; 183 | } 184 | interface SnapshotSerializerColors { 185 | comment: SnapshotSerializerColor; 186 | content: SnapshotSerializerColor; 187 | prop: SnapshotSerializerColor; 188 | tag: SnapshotSerializerColor; 189 | value: SnapshotSerializerColor; 190 | } 191 | interface SnapshotSerializerPlugin { 192 | print(val:any, serialize:((val:any) => string), indent:((str:string) => string), opts:SnapshotSerializerOptions, colors: SnapshotSerializerColors) : string; 193 | test(val:any) : boolean; 194 | } 195 | 196 | /** The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. */ 197 | interface Expect { 198 | /** 199 | * The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. 200 | */ 201 | (actual: any): Matchers; 202 | anything(): any; 203 | /** Matches anything that was created with the given constructor. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 204 | any(classType: any): any; 205 | /** Matches any array made up entirely of elements in the provided array. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 206 | arrayContaining(arr: any[]): any; 207 | /** Verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. */ 208 | assertions(num: number): void; 209 | /** Verifies that at least one assertion is called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. */ 210 | hasAssertions(): void; 211 | /** You can use `expect.extend` to add your own matchers to Jest. */ 212 | extend(obj: ExpectExtendMap): void; 213 | /** Adds a module to format application-specific data structures for serialization. */ 214 | addSnapshotSerializer(serializer: SnapshotSerializerPlugin) : void; 215 | /** Matches any object that recursively matches the provided keys. This is often handy in conjunction with other asymmetric matchers. */ 216 | objectContaining(obj: {}): any; 217 | /** Matches any string that contains the exact provided string */ 218 | stringMatching(str: string | RegExp): any; 219 | } 220 | 221 | interface Matchers { 222 | /** If you know how to test something, `.not` lets you test its opposite. */ 223 | not: Matchers; 224 | /** Use resolves to unwrap the value of a fulfilled promise so any other matcher can be chained. If the promise is rejected the assertion fails. */ 225 | resolves: Matchers>; 226 | /** Unwraps the reason of a rejected promise so any other matcher can be chained. If the promise is fulfilled the assertion fails. */ 227 | rejects: Matchers>; 228 | lastCalledWith(...args: any[]): R; 229 | /** Checks that a value is what you expect. It uses `===` to check strict equality. Don't use `toBe` with floating-point numbers. */ 230 | toBe(expected: any): R; 231 | /** Ensures that a mock function is called. */ 232 | toBeCalled(): R; 233 | /** Ensure that a mock function is called with specific arguments. */ 234 | toBeCalledWith(...args: any[]): R; 235 | /** Using exact equality with floating point numbers is a bad idea. Rounding means that intuitive things fail. */ 236 | toBeCloseTo(expected: number, delta?: number): R; 237 | /** Ensure that a variable is not undefined. */ 238 | toBeDefined(): R; 239 | /** When you don't care what a value is, you just want to ensure a value is false in a boolean context. */ 240 | toBeFalsy(): R; 241 | /** For comparing floating point numbers. */ 242 | toBeGreaterThan(expected: number): R; 243 | /** For comparing floating point numbers. */ 244 | toBeGreaterThanOrEqual(expected: number): R; 245 | /** Ensure that an object is an instance of a class. This matcher uses `instanceof` underneath. */ 246 | toBeInstanceOf(expected: any): R 247 | /** For comparing floating point numbers. */ 248 | toBeLessThan(expected: number): R; 249 | /** For comparing floating point numbers. */ 250 | toBeLessThanOrEqual(expected: number): R; 251 | /** This is the same as `.toBe(null)` but the error messages are a bit nicer. So use `.toBeNull()` when you want to check that something is null. */ 252 | toBeNull(): R; 253 | /** Use when you don't care what a value is, you just want to ensure a value is true in a boolean context. In JavaScript, there are six falsy values: `false`, `0`, `''`, `null`, `undefined`, and `NaN`. Everything else is truthy. */ 254 | toBeTruthy(): R; 255 | /** Used to check that a variable is undefined. */ 256 | toBeUndefined(): R; 257 | /** Used to check that a variable is NaN. */ 258 | toBeNaN(): R; 259 | /** Used when you want to check that an item is in a list. For testing the items in the list, this uses `===`, a strict equality check. */ 260 | toContain(expected: any): R; 261 | /** Used when you want to check that an item is in a list. For testing the items in the list, this matcher recursively checks the equality of all fields, rather than checking for object identity. */ 262 | toContainEqual(expected: any): R; 263 | /** Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity. */ 264 | toEqual(expected: any): R; 265 | /** Ensures that a mock function is called. */ 266 | toHaveBeenCalled(): R; 267 | /** Ensures that a mock function is called an exact number of times. */ 268 | toHaveBeenCalledTimes(expected: number): R; 269 | /** Ensure that a mock function is called with specific arguments. */ 270 | toHaveBeenCalledWith(...params: any[]): R; 271 | /** If you have a mock function, you can use `.toHaveBeenLastCalledWith` to test what arguments it was last called with. */ 272 | toHaveBeenLastCalledWith(...params: any[]): R; 273 | /** Used to check that an object has a `.length` property and it is set to a certain numeric value. */ 274 | toHaveLength(expected: number): R; 275 | toHaveProperty(propertyPath: string, value?: any): R; 276 | /** Check that a string matches a regular expression. */ 277 | toMatch(expected: string | RegExp): R; 278 | /** Used to check that a JavaScript object matches a subset of the properties of an objec */ 279 | toMatchObject(expected: {}): R; 280 | /** This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information. */ 281 | toMatchSnapshot(snapshotName?: string): R; 282 | /** Used to test that a function throws when it is called. */ 283 | toThrow(error?: string | Constructable | RegExp): R; 284 | /** If you want to test that a specific error is thrown inside a function. */ 285 | toThrowError(error?: string | Constructable | RegExp): R; 286 | /** Used to test that a function throws a error matching the most recent snapshot when it is called. */ 287 | toThrowErrorMatchingSnapshot(): R; 288 | } 289 | 290 | interface Constructable { 291 | new (...args: any[]): any 292 | } 293 | 294 | interface Mock extends Function, MockInstance { 295 | new (): T; 296 | (...args: any[]): any; 297 | } 298 | 299 | interface SpyInstance extends MockInstance { 300 | mockRestore(): void; 301 | } 302 | 303 | /** 304 | * Wrap module with mock definitions 305 | * @example 306 | * jest.mock("../api"); 307 | * import { Api } from "../api"; 308 | * 309 | * const myApi: jest.Mocked = new Api() as any; 310 | * myApi.myApiMethod.mockImplementation(() => "test"); 311 | */ 312 | type Mocked = { 313 | [P in keyof T]: T[P] & MockInstance; 314 | } & T; 315 | 316 | interface MockInstance { 317 | mock: MockContext; 318 | mockClear(): void; 319 | mockReset(): void; 320 | mockImplementation(callback: Function): Mock; 321 | mockImplementationOnce(callback: Function): Mock; 322 | mockReturnThis(): Mock; 323 | mockReturnValue(value: any): Mock; 324 | mockReturnValueOnce(value: any): Mock; 325 | } 326 | 327 | interface MockContext { 328 | calls: any[][]; 329 | instances: T[]; 330 | } 331 | } 332 | 333 | //Jest ships with a copy of Jasmine. They monkey-patch its APIs and divergence/deprecation are expected. 334 | //Relevant parts of Jasmine's API are below so they can be changed and removed over time. 335 | //This file can't reference jasmine.d.ts since the globals aren't compatible. 336 | 337 | declare function spyOn(object: any, method: string): jasmine.Spy; 338 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */ 339 | declare function pending(reason?: string): void; 340 | /** Fails a test when called within one. */ 341 | declare function fail(error?: any): void; 342 | declare namespace jasmine { 343 | var DEFAULT_TIMEOUT_INTERVAL: number; 344 | var clock: () => Clock; 345 | function any(aclass: any): Any; 346 | function anything(): Any; 347 | function arrayContaining(sample: any[]): ArrayContaining; 348 | function objectContaining(sample: any): ObjectContaining; 349 | function createSpy(name: string, originalFn?: Function): Spy; 350 | function createSpyObj(baseName: string, methodNames: any[]): any; 351 | function createSpyObj(baseName: string, methodNames: any[]): T; 352 | function pp(value: any): string; 353 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 354 | function addMatchers(matchers: CustomMatcherFactories): void; 355 | function stringMatching(value: string | RegExp): Any; 356 | 357 | interface Clock { 358 | install(): void; 359 | uninstall(): void; 360 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */ 361 | tick(ms: number): void; 362 | mockDate(date?: Date): void; 363 | } 364 | 365 | interface Any { 366 | new (expectedClass: any): any; 367 | jasmineMatches(other: any): boolean; 368 | jasmineToString(): string; 369 | } 370 | 371 | interface ArrayContaining { 372 | new (sample: any[]): any; 373 | asymmetricMatch(other: any): boolean; 374 | jasmineToString(): string; 375 | } 376 | 377 | interface ObjectContaining { 378 | new (sample: any): any; 379 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean; 380 | jasmineToString(): string; 381 | } 382 | 383 | interface Spy { 384 | (...params: any[]): any; 385 | identity: string; 386 | and: SpyAnd; 387 | calls: Calls; 388 | mostRecentCall: { args: any[]; }; 389 | argsForCall: any[]; 390 | wasCalled: boolean; 391 | } 392 | 393 | interface SpyAnd { 394 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */ 395 | callThrough(): Spy; 396 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */ 397 | returnValue(val: any): Spy; 398 | /** By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list. */ 399 | returnValues(...values: any[]): Spy; 400 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */ 401 | callFake(callback: Function): Spy; 402 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */ 403 | throwError(msg: string): Spy; 404 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */ 405 | stub(): Spy; 406 | } 407 | 408 | interface Calls { 409 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. */ 410 | any(): boolean; 411 | /** By chaining the spy with calls.count(), will return the number of times the spy was called */ 412 | count(): number; 413 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index */ 414 | argsFor(index: number): any[]; 415 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls */ 416 | allArgs(): any[]; 417 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls */ 418 | all(): CallInfo[]; 419 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call */ 420 | mostRecent(): CallInfo; 421 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call */ 422 | first(): CallInfo; 423 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy */ 424 | reset(): void; 425 | } 426 | 427 | interface CallInfo { 428 | /** The context (the this) for the call */ 429 | object: any; 430 | /** All arguments passed to the call */ 431 | args: any[]; 432 | /** The return value of the call */ 433 | returnValue: any; 434 | } 435 | 436 | interface CustomMatcherFactories { 437 | [index: string]: CustomMatcherFactory; 438 | } 439 | 440 | interface CustomMatcherFactory { 441 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher; 442 | } 443 | 444 | interface MatchersUtil { 445 | equals(a: any, b: any, customTesters?: Array): boolean; 446 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean; 447 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string; 448 | } 449 | 450 | interface CustomEqualityTester { 451 | (first: any, second: any): boolean; 452 | } 453 | 454 | interface CustomMatcher { 455 | compare(actual: T, expected: T): CustomMatcherResult; 456 | compare(actual: any, expected: any): CustomMatcherResult; 457 | } 458 | 459 | interface CustomMatcherResult { 460 | pass: boolean; 461 | message: string | (() => string); 462 | } 463 | 464 | interface ArrayLike { 465 | length: number; 466 | [n: number]: T; 467 | } 468 | } 469 | --------------------------------------------------------------------------------