apollo-tracer
Version:
Trace collection library for Node.js GraphQL servers
327 lines (303 loc) • 10.7 kB
JavaScript
import { makeExecutableSchema } from 'graphql-tools';
import { expect } from 'chai';
import { graphql } from 'graphql';
import { Tracer, decorateWithTracer, addTracingToResolvers } from '../src/Tracer.js';
const request = require('request'); // just to override it
describe('Tracer', () => {
const shorthand = `
type RootQuery {
returnArg(name: String): String
returnErr: String
returnPromiseArg(name: String): String
returnPromiseErr: String
returnUndefined: Int
returnNull: Int
}
schema {
query: RootQuery
}
`;
const resolver = {
RootQuery: {
returnArg: (root, { name }) => {
// return `${name}`;
return name;
},
returnErr: () => {
throw new Error('aargh!');
},
returnPromiseArg: (root, { name }) => {
return Promise.resolve(name);
},
returnPromiseErr: () => {
return new Promise((resolve, reject) => {
setTimeout(() => { reject(new Error('err')); }, 0);
});
},
returnUndefined: () => undefined,
returnNull: () => null,
},
};
const t1 = new Tracer({ TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A' });
const jsSchema = makeExecutableSchema({
typeDefs: shorthand,
resolvers: resolver,
allowUndefinedInResolve: true,
});
addTracingToResolvers(jsSchema);
it('throws an error if you construct it without valid TRACER_APP_KEY', () => {
expect(() => {
// eslint-disable-next-line no-unused-vars
const t = new Tracer({ TRACER_APP_KEY: 'uga' });
}).to.throw('Tracer requires a well-formatted TRACER_APP_KEY');
});
it('does basic tracing for non-promises', () => {
const testQuery = `{
returnArg(name: "it")
}`;
const tracer = t1.newLoggerInstance();
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
});
});
it('does basic tracing for non-promise throwing an error', () => {
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
});
});
it('does basic tracing for promises', () => {
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnPromiseArg(name: "it")
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
});
});
it('does basic tracing for promise that throws an error', () => {
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
});
});
it('does not throw an error if the resolve function returns undefined', () => {
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnUndefined
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then((res) => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
expect(report.events[1].data.returnedUndefined).to.equal(true);
expect(res.data.returnUndefined).to.equal(null);
expect(res.errors).to.equal(undefined);
});
});
it('does not throw an error if the resolve function returns null', () => {
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnNull
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then((res) => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
expect(report.events[1].data.returnedNull).to.equal(true);
expect(res.data.returnNull).to.equal(null);
expect(res.errors).to.equal(undefined);
});
});
it('does not add tracing to schema if already added', () => {
// same test as previous, just calling addTracingToResolvers again
// and making sure we still log the expected number of events
addTracingToResolvers(jsSchema);
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
const report = tracer.report('');
expect(report.events.length).to.equal(2);
});
});
// decorateWithTracer tests
it('reports a returnedNull when resolver returns null', () => {
const fn = () => null;
const decorated = decorateWithTracer(fn, { functionName: 'Test.test' });
const tracer = t1.newLoggerInstance();
decorated(null, null, { tracer });
expect(tracer.report().events[1].data.returnedNull).to.equal(true);
});
it('reports a returnedUndefined when resolver returns null', () => {
const fn = () => undefined;
const decorated = decorateWithTracer(fn, { functionName: 'Test.test' });
const tracer = t1.newLoggerInstance();
decorated(null, null, { tracer });
expect(tracer.report().events[1].data.returnedUndefined).to.equal(true);
});
it('reports a tracer.error when tracer decorator makes a boo boo', () => {
const fn = () => { return { then() { throw new Error('boo boo'); } }; };
const decorated = decorateWithTracer(fn, { functionName: 'Test.test' });
const tracer = t1.newLoggerInstance();
decorated(null, null, { tracer });
expect(tracer.report().events[1].type).to.equal('tracer.error');
expect(tracer.report().events[1].data.tracerError.message).to.equal('boo boo');
});
// send report tests
it('calls sendReport with the right arguments', () => {
const t2 = new Tracer({ TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A' });
let interceptedReport = null;
// test harness for submit
t2.sendReport = (report) => { interceptedReport = report; };
const tracer = t2.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
const expected = [
'TRACER_APP_KEY',
'events',
'queryId',
'startHrTime',
'startTime',
'tracerApiVersion',
];
expect(Object.keys(interceptedReport).sort()).to.deep.equal(expected);
expect(interceptedReport.events.length).to.equal(2);
});
});
it('calls request with the right arguments to report', () => {
let interceptedReport = null;
// test harness for submit
const realRequest = request.Request;
request.Request = (params) => {
interceptedReport = params.json;
};
const tracer = t1.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
const expected = [
'TRACER_APP_KEY',
'events',
'queryId',
'startHrTime',
'startTime',
'tracerApiVersion',
];
request.Request = realRequest;
expect(Object.keys(interceptedReport).sort()).to.deep.equal(expected);
expect(interceptedReport.events.length).to.equal(2);
});
});
it('filters events in sendReport if you tell it to', () => {
const t2 = new Tracer({
TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A',
reportFilterFn: (e) => (e.type !== 'resolver.end'),
});
let interceptedReport = null;
// test harness for submit
const realRequest = request.Request;
request.Request = (params) => {
interceptedReport = params.json;
};
const tracer = t2.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
request.Request = realRequest;
expect(interceptedReport.events.length).to.equal(1);
expect(interceptedReport.events[0].type).to.equal('resolver.start');
});
});
it('does not send report if sendReports is false', () => {
const t2 = new Tracer({
TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A',
sendReports: false,
});
let interceptedReport = null;
// test harness for submit
t2.sendReport = (report) => { interceptedReport = report; };
const tracer = t2.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
expect(interceptedReport).to.equal(null);
});
});
it('prints an error if request fails in sendReport', () => {
// const tracer = t1.newLoggerInstance();
const realRequest = request.Request;
let interceptedMsg;
const realConsoleError = console.error;
// XXX yeah... maybe use sinon?
console.error = (msg, err) => { interceptedMsg = [msg, err]; };
request.Request = ({ callback }) => {
callback(new Error('nope'));
};
t1.sendReport('uga');
request.Request = realRequest;
console.error = realConsoleError;
expect(interceptedMsg[0]).to.equal('Error trying to report to tracer backend:');
expect(interceptedMsg[1]).to.equal('nope');
});
it('includes proxy in request if you tell it to', () => {
const t2 = new Tracer({
TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A',
proxy: 'myproxy.co',
});
let interceptedReport = null;
// test harness for submit
const realRequest = request.Request;
request.Request = (params) => {
interceptedReport = params;
};
const tracer = t2.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
request.Request = realRequest;
expect(interceptedReport.proxy).to.equal('myproxy.co');
});
});
it('does not include proxy in request if excluded', () => {
const t2 = new Tracer({
TRACER_APP_KEY: 'BDE05C83-E58F-4837-8D9A-9FB5EA605D2A',
});
let interceptedReport = null;
// test harness for submit
const realRequest = request.Request;
request.Request = (params) => {
interceptedReport = params;
};
const tracer = t2.newLoggerInstance();
const testQuery = `{
returnPromiseErr
}`;
return graphql(jsSchema, testQuery, null, { tracer }).then(() => {
tracer.submit();
request.Request = realRequest;
expect(interceptedReport.proxy).to.equal(undefined);
});
});
// TODO test calling sendReport with non-json
});