Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automatic schema reporting to Apollo Graph Manager #4084

Merged
merged 31 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ad3073e
Initial Schema Reporting commit
May 14, 2020
764b1ce
Remove apollo link
May 12, 2020
67c57bf
Rename all schema hash and schema id to executable schema Id
May 12, 2020
de1523b
Remove override executable schema id generator
May 13, 2020
0413c20
Add and fix tests
May 14, 2020
75d9f94
Add simple caching
May 14, 2020
1ef5ec5
Add documentation
May 14, 2020
4b00bf2
Add schema id generator tests
May 14, 2020
968e94b
Increase delay time
May 14, 2020
6af8467
Fix equality
May 14, 2020
f094ebe
Address comments
May 15, 2020
465ebe5
Address comments
May 17, 2020
6ac44da
Cleanup
May 17, 2020
15fd0e6
Fix sha256 test
May 17, 2020
d9f0d8e
Fix plugin tests
May 17, 2020
977a89e
Prettier
May 18, 2020
f9751fe
Parially address comments
May 18, 2020
2551f93
Add error handling around http
May 18, 2020
490a02e
Adds test
May 18, 2020
ce2c8bf
Don't normalize GraphQL Schema in apollo-engine-reporting
May 18, 2020
a25a785
Apply suggestions from my own code review
abernix May 19, 2020
f08b1ca
Merge branch 'release-2.14.0' into jsegaran/schema_reporting
abernix May 19, 2020
5b8a766
Fix mistakes in my own GitHub suggestion.
abernix May 19, 2020
fd196fb
Separate JSON parsing failures from general `fetch` errors.
abernix May 19, 2020
4836f75
Tweak error message phrasing.
abernix May 19, 2020
3ddddd4
Apply specificity to the errors we are expecting to encounter.
abernix May 19, 2020
003b435
DRY up repeated error message for unexpected response shape.
abernix May 19, 2020
cb56ca2
Update "Engine" reference to "Apollo Graph Manager".
abernix May 19, 2020
e3b95e9
Fix my own markdown error.
abernix May 19, 2020
8598437
Just allow `any` type for the unexpected error msg.
abernix May 19, 2020
bf5379a
Don't dynamically import `crypto` to use `createHash`.
abernix May 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/apollo-engine-reporting-protobuf/src/reports.proto
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ message ReportHeader {
// eg "current", "prod"
string schema_tag = 10;
// The hex representation of the sha512 of the introspection response
string schema_hash = 11;
string deprecated_schema_hash = 11;
jsegaran marked this conversation as resolved.
Show resolved Hide resolved

// An id that is used to represent the schema to Apollo Graph Manager
string executable_schema_id = 12;

reserved 3; // removed string service = 3;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/apollo-engine-reporting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@
"node": ">=6.0"
},
"dependencies": {
"@types/sha.js": "^2.4.0",
"@types/uuid": "^7.0.3",
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
"apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf",
"apollo-graphql": "^0.4.0",
"apollo-link": "^1.2.14",
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
"apollo-server-caching": "file:../apollo-server-caching",
"apollo-server-env": "file:../apollo-server-env",
"apollo-server-errors": "file:../apollo-server-errors",
"apollo-server-plugin-base": "file:../apollo-server-plugin-base",
"apollo-server-types": "file:../apollo-server-types",
"async-retry": "^1.2.1"
"async-retry": "^1.2.1",
"sha.js": "^2.4.11",
"uuid": "^8.0.0"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
Expand Down
58 changes: 56 additions & 2 deletions packages/apollo-engine-reporting/src/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
signatureCacheKey,
handleLegacyOptions,
EngineReportingOptions,
} from '../agent';
EngineReportingOptions, computeExecutableSchemaId
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
} from "../agent";
import { buildSchema } from "graphql";

describe('signature cache key', () => {
it('generates without the operationName', () => {
Expand All @@ -16,6 +17,59 @@ describe('signature cache key', () => {
});
});

describe('Executable Schema Id', () => {
const unsortedGQLSchemaDocument = `
directive @example on FIELD
union AccountOrUser = Account | User
type Query {
userOrAccount(name: String, id: String): AccountOrUser
}

type User {
accounts: [Account!]
email: String
name: String!
}

type Account {
name: String!
id: ID!
}
`;

const sortedGQLSchemaDocument = `
directive @example on FIELD
union AccountOrUser = Account | User

type Account {
name: String!
id: ID!
}

type Query {
userOrAccount(id: String, name: String): AccountOrUser
}

type User {
accounts: [Account!]
email: String
name: String!
}

`;
it('to be normalized for graphql schemas', () => {
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).toEqual(
computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument))
);
});
it('to not be normalized for strings', () => {
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
expect(computeExecutableSchemaId(unsortedGQLSchemaDocument)).not.toEqual(
computeExecutableSchemaId(sortedGQLSchemaDocument)
);
});
});


describe("test handleLegacyOptions(), which converts the deprecated privateVariable and privateHeaders options to the new options' formats", () => {
it('Case 1: privateVariables/privateHeaders == False; same as all', () => {
const optionsPrivateFalse: EngineReportingOptions<any> = {
Expand Down
173 changes: 165 additions & 8 deletions packages/apollo-engine-reporting/src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { graphql, GraphQLError } from 'graphql';
import {
graphql,
GraphQLError,
GraphQLSchema,
lexicographicSortSchema,
printSchema,
stripIgnoredCharacters,
} from 'graphql';
import { Request } from 'node-fetch';
import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin';
import { Headers } from 'apollo-server-env';
import { AddTraceArgs } from '../agent';
import { AddTraceArgs, computeExecutableSchemaId } from "../agent";
import { Trace } from 'apollo-engine-reporting-protobuf';
import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness';
import { sha256 } from 'sha.js';
import { isString } from 'util';

it('trace construction', async () => {
const typeDefs = `
const typeDefs = `
type User {
id: Int
name: String
Expand All @@ -31,7 +39,7 @@ it('trace construction', async () => {
}
`;

const query = `
const query = `
query q {
author(id: 5) {
name
Expand All @@ -43,17 +51,166 @@ it('trace construction', async () => {
}
`;

describe('schema reporting', () => {
const schema = makeExecutableSchema({ typeDefs });
addMockFunctionsToSchema({ schema });

const addTrace = jest.fn();
const startSchemaReporting = jest.fn();
const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId);

beforeEach(() => {
addTrace.mockClear();
startSchemaReporting.mockClear();
executableSchemaIdGenerator.mockClear();
});

it('starts reporing if enabled', async () => {
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
},
addTrace,
startSchemaReporting,
executableSchemaIdGenerator,
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: printSchema(schema),
executableSchemaId: executableSchemaIdGenerator(schema),
});
});

it('uses the override schema', async () => {
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
experimental_overrideReportedSchema: typeDefs,
},
addTrace,
startSchemaReporting,
executableSchemaIdGenerator,
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

const expectedExecutableSchemaId = executableSchemaIdGenerator(typeDefs);
expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: typeDefs,
executableSchemaId: expectedExecutableSchemaId,
});

// Get the first argument from the first time this is called.
// Not using called with because that has to be exhaustive and this isn't
// testing trace generation
expect(addTrace.mock.calls[0][0].executableSchemaId).toBe(
expectedExecutableSchemaId,
);
jsegaran marked this conversation as resolved.
Show resolved Hide resolved
});

it('uses the same executable schema id for metric reporting', async () => {
const pluginInstance = plugin(
{
experimental_schemaReporting: true,
},
addTrace,
startSchemaReporting,
executableSchemaIdGenerator,
);

await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
query,
operationName: 'q',
extensions: {
clientName: 'testing suite',
},
http: new Request('http://localhost:123/foo'),
},
executor: async ({ request: { query: source } }) => {
return await graphql({
schema,
source,
});
},
});

const expectedExecutableSchemaId = executableSchemaIdGenerator(schema);
expect(startSchemaReporting).toBeCalledTimes(1);
expect(startSchemaReporting).toBeCalledWith({
executableSchema: printSchema(schema),
executableSchemaId: expectedExecutableSchemaId,
});
// Get the first argument from the first time this is called.
// Not using called with because that has to be exhaustive and this isn't
// testing trace generation
expect(addTrace.mock.calls[0][0].executableSchemaId).toBe(
expectedExecutableSchemaId,
);
});
});

it('trace construction', async () => {
const schema = makeExecutableSchema({ typeDefs });
addMockFunctionsToSchema({ schema });

const traces: Array<AddTraceArgs> = [];
async function addTrace(args: AddTraceArgs) {
traces.push(args);
}
const startSchemaReporting = jest.fn();
const executableSchemaIdGenerator = jest.fn();

const pluginInstance = plugin({ /* no options!*/ }, addTrace);
const pluginInstance = plugin(
{
/* no options!*/
},
addTrace,
startSchemaReporting,
executableSchemaIdGenerator,
);

pluginTestHarness({
await pluginTestHarness({
pluginInstance,
schema,
graphqlRequest: {
Expand Down Expand Up @@ -260,7 +417,7 @@ describe('variableJson output for sendVariableValues transform: custom function
).toEqual(JSON.stringify(null));
});

const errorThrowingModifier = (input: {
const errorThrowingModifier = (_input: {
variables: Record<string, any>;
}): Record<string, any> => {
throw new GraphQLError('testing error handling');
Expand Down
Loading