diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7181117b3..394e0664be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The version headers in this history reflect the versions of Apollo Server itself ## vNEXT (minor!) - `apollo-server-core`: Usage reporting no longer sends a "client reference ID" to Apollo Studio (along with the client name and client version). This little-used feature has not been documented [since 2019](https://github.com/apollographql/apollo-server/pull/3180) and is currently entirely ignored by Apollo Studio. This is technically incompatible as the interface `ClientInfo` no longer has the field `clientReferenceId`; if you were one of the few users who explicitly set this field and you get a TypeScript compilation failure upon upgrading to v3.6.0, just stop using the field. [PR #5890](https://github.com/apollographql/apollo-server/pull/5890) +- `apollo-server-core`: Preliminary support for referenced field reporting. THIS ENTRY NEEDS TO BE EXPANDED BEFORE THE v3.6.0 RELEASE. [Issue #5708](https://github.com/apollographql/apollo-server/issues/5708) [PR #5956](https://github.com/apollographql/apollo-server/pull/5956) - `apollo-server-core`: Remove dependency on `apollo-graphql` package (by inlining the code which generates usage reporting signatures). That package has not yet been published with a `graphql@16` peer dependency, so Apollo Server v3.5 did not fully support `graphql@16` without overriding peer dependencies. [Issue #5941](https://github.com/apollographql/apollo-server/issues/5941) [PR #5955](https://github.com/apollographql/apollo-server/pull/5955) ## v3.5.0 diff --git a/packages/apollo-reporting-protobuf/generated/protobuf.d.ts b/packages/apollo-reporting-protobuf/generated/protobuf.d.ts index a94f5e1da16..79fb9ea0b7d 100644 --- a/packages/apollo-reporting-protobuf/generated/protobuf.d.ts +++ b/packages/apollo-reporting-protobuf/generated/protobuf.d.ts @@ -32,9 +32,6 @@ export interface ITrace { /** Trace clientVersion */ clientVersion?: (string|null); - /** Trace clientAddress */ - clientAddress?: (string|null); - /** Trace http */ http?: (Trace.IHTTP|null); @@ -99,9 +96,6 @@ export class Trace implements ITrace { /** Trace clientVersion. */ public clientVersion: string; - /** Trace clientAddress. */ - public clientAddress: string; - /** Trace http. */ public http?: (Trace.IHTTP|null); @@ -2323,179 +2317,90 @@ export class TypeStat implements ITypeStat { public toJSON(): { [k: string]: any }; } -/** Properties of a Field. */ -export interface IField { +/** Properties of a ReferencedFieldsForType. */ +export interface IReferencedFieldsForType { - /** Field name */ - name?: (string|null); + /** ReferencedFieldsForType fieldNames */ + fieldNames?: (string[]|null); - /** Field returnType */ - returnType?: (string|null); + /** ReferencedFieldsForType isInterface */ + isInterface?: (boolean|null); } -/** Represents a Field. */ -export class Field implements IField { +/** Represents a ReferencedFieldsForType. */ +export class ReferencedFieldsForType implements IReferencedFieldsForType { /** - * Constructs a new Field. + * Constructs a new ReferencedFieldsForType. * @param [properties] Properties to set */ - constructor(properties?: IField); + constructor(properties?: IReferencedFieldsForType); - /** Field name. */ - public name: string; + /** ReferencedFieldsForType fieldNames. */ + public fieldNames: string[]; - /** Field returnType. */ - public returnType: string; + /** ReferencedFieldsForType isInterface. */ + public isInterface: boolean; /** - * Creates a new Field instance using the specified properties. + * Creates a new ReferencedFieldsForType instance using the specified properties. * @param [properties] Properties to set - * @returns Field instance + * @returns ReferencedFieldsForType instance */ - public static create(properties?: IField): Field; + public static create(properties?: IReferencedFieldsForType): ReferencedFieldsForType; /** - * Encodes the specified Field message. Does not implicitly {@link Field.verify|verify} messages. - * @param message Field message or plain object to encode + * Encodes the specified ReferencedFieldsForType message. Does not implicitly {@link ReferencedFieldsForType.verify|verify} messages. + * @param message ReferencedFieldsForType message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ - public static encode(message: IField, writer?: $protobuf.Writer): $protobuf.Writer; + public static encode(message: IReferencedFieldsForType, writer?: $protobuf.Writer): $protobuf.Writer; /** - * Encodes the specified Field message, length delimited. Does not implicitly {@link Field.verify|verify} messages. - * @param message Field message or plain object to encode + * Encodes the specified ReferencedFieldsForType message, length delimited. Does not implicitly {@link ReferencedFieldsForType.verify|verify} messages. + * @param message ReferencedFieldsForType message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ - public static encodeDelimited(message: IField, writer?: $protobuf.Writer): $protobuf.Writer; + public static encodeDelimited(message: IReferencedFieldsForType, writer?: $protobuf.Writer): $protobuf.Writer; /** - * Decodes a Field message from the specified reader or buffer. + * Decodes a ReferencedFieldsForType message from the specified reader or buffer. * @param reader Reader or buffer to decode from * @param [length] Message length if known beforehand - * @returns Field + * @returns ReferencedFieldsForType * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Field; + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ReferencedFieldsForType; /** - * Decodes a Field message from the specified reader or buffer, length delimited. + * Decodes a ReferencedFieldsForType message from the specified reader or buffer, length delimited. * @param reader Reader or buffer to decode from - * @returns Field + * @returns ReferencedFieldsForType * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Field; + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ReferencedFieldsForType; /** - * Verifies a Field message. + * Verifies a ReferencedFieldsForType message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ public static verify(message: { [k: string]: any }): (string|null); /** - * Creates a plain object from a Field message. Also converts values to other types if specified. - * @param message Field + * Creates a plain object from a ReferencedFieldsForType message. Also converts values to other types if specified. + * @param message ReferencedFieldsForType * @param [options] Conversion options * @returns Plain object */ - public static toObject(message: Field, options?: $protobuf.IConversionOptions): { [k: string]: any }; + public static toObject(message: ReferencedFieldsForType, options?: $protobuf.IConversionOptions): { [k: string]: any }; /** - * Converts this Field to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} - -/** Properties of a Type. */ -export interface IType { - - /** Type name */ - name?: (string|null); - - /** Type field */ - field?: (IField[]|null); -} - -/** Represents a Type. */ -export class Type implements IType { - - /** - * Constructs a new Type. - * @param [properties] Properties to set - */ - constructor(properties?: IType); - - /** Type name. */ - public name: string; - - /** Type field. */ - public field: IField[]; - - /** - * Creates a new Type instance using the specified properties. - * @param [properties] Properties to set - * @returns Type instance - */ - public static create(properties?: IType): Type; - - /** - * Encodes the specified Type message. Does not implicitly {@link Type.verify|verify} messages. - * @param message Type message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IType, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified Type message, length delimited. Does not implicitly {@link Type.verify|verify} messages. - * @param message Type message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IType, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a Type message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns Type - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Type; - - /** - * Decodes a Type message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns Type - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Type; - - /** - * Verifies a Type message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates a plain object from a Type message. Also converts values to other types if specified. - * @param message Type - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: Type, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this Type to JSON. + * Converts this ReferencedFieldsForType to JSON. * @returns JSON object */ public toJSON(): { [k: string]: any }; @@ -2700,6 +2605,9 @@ export interface ITracesAndStats { /** TracesAndStats statsWithContext */ statsWithContext?: ($protobuf.ToArray|IContextualizedStats[]|null); + /** TracesAndStats referencedFieldsByType */ + referencedFieldsByType?: ({ [k: string]: IReferencedFieldsForType }|null); + /** TracesAndStats internalTracesContributingToStats */ internalTracesContributingToStats?: ((ITrace|Uint8Array)[]|null); } @@ -2719,6 +2627,9 @@ export class TracesAndStats implements ITracesAndStats { /** TracesAndStats statsWithContext. */ public statsWithContext: IContextualizedStats[]; + /** TracesAndStats referencedFieldsByType. */ + public referencedFieldsByType: { [k: string]: IReferencedFieldsForType }; + /** TracesAndStats internalTracesContributingToStats. */ public internalTracesContributingToStats: (ITrace|Uint8Array)[]; diff --git a/packages/apollo-reporting-protobuf/generated/protobuf.js b/packages/apollo-reporting-protobuf/generated/protobuf.js index 913efe2e80f..68b3768bc79 100644 --- a/packages/apollo-reporting-protobuf/generated/protobuf.js +++ b/packages/apollo-reporting-protobuf/generated/protobuf.js @@ -25,7 +25,6 @@ $root.Trace = (function() { * @property {Trace.IDetails|null} [details] Trace details * @property {string|null} [clientName] Trace clientName * @property {string|null} [clientVersion] Trace clientVersion - * @property {string|null} [clientAddress] Trace clientAddress * @property {Trace.IHTTP|null} [http] Trace http * @property {Trace.ICachePolicy|null} [cachePolicy] Trace cachePolicy * @property {Trace.IQueryPlanNode|null} [queryPlan] Trace queryPlan @@ -131,14 +130,6 @@ $root.Trace = (function() { */ Trace.prototype.clientVersion = ""; - /** - * Trace clientAddress. - * @member {string} clientAddress - * @memberof Trace - * @instance - */ - Trace.prototype.clientAddress = ""; - /** * Trace http. * @member {Trace.IHTTP|null|undefined} http @@ -237,8 +228,6 @@ $root.Trace = (function() { writer.uint32(/* id 7, wireType 2 =*/58).string(message.clientName); if (message.clientVersion != null && Object.hasOwnProperty.call(message, "clientVersion")) writer.uint32(/* id 8, wireType 2 =*/66).string(message.clientVersion); - if (message.clientAddress != null && Object.hasOwnProperty.call(message, "clientAddress")) - writer.uint32(/* id 9, wireType 2 =*/74).string(message.clientAddress); if (message.http != null && Object.hasOwnProperty.call(message, "http")) $root.Trace.HTTP.encode(message.http, writer.uint32(/* id 10, wireType 2 =*/82).fork()).ldelim(); if (message.durationNs != null && Object.hasOwnProperty.call(message, "durationNs")) @@ -329,9 +318,6 @@ $root.Trace = (function() { case 8: message.clientVersion = reader.string(); break; - case 9: - message.clientAddress = reader.string(); - break; case 10: message.http = $root.Trace.HTTP.decode(reader, reader.uint32()); break; @@ -429,9 +415,6 @@ $root.Trace = (function() { if (message.clientVersion != null && message.hasOwnProperty("clientVersion")) if (!$util.isString(message.clientVersion)) return "clientVersion: string expected"; - if (message.clientAddress != null && message.hasOwnProperty("clientAddress")) - if (!$util.isString(message.clientAddress)) - return "clientAddress: string expected"; if (message.http != null && message.hasOwnProperty("http")) { var error = $root.Trace.HTTP.verify(message.http); if (error) @@ -484,7 +467,6 @@ $root.Trace = (function() { object.details = null; object.clientName = ""; object.clientVersion = ""; - object.clientAddress = ""; object.http = null; if ($util.Long) { var long = new $util.Long(0, 0, true); @@ -513,8 +495,6 @@ $root.Trace = (function() { object.clientName = message.clientName; if (message.clientVersion != null && message.hasOwnProperty("clientVersion")) object.clientVersion = message.clientVersion; - if (message.clientAddress != null && message.hasOwnProperty("clientAddress")) - object.clientAddress = message.clientAddress; if (message.http != null && message.hasOwnProperty("http")) object.http = $root.Trace.HTTP.toObject(message.http, options); if (message.durationNs != null && message.hasOwnProperty("durationNs")) @@ -5978,25 +5958,26 @@ $root.TypeStat = (function() { return TypeStat; })(); -$root.Field = (function() { +$root.ReferencedFieldsForType = (function() { /** - * Properties of a Field. - * @exports IField - * @interface IField - * @property {string|null} [name] Field name - * @property {string|null} [returnType] Field returnType + * Properties of a ReferencedFieldsForType. + * @exports IReferencedFieldsForType + * @interface IReferencedFieldsForType + * @property {Array.|null} [fieldNames] ReferencedFieldsForType fieldNames + * @property {boolean|null} [isInterface] ReferencedFieldsForType isInterface */ /** - * Constructs a new Field. - * @exports Field - * @classdesc Represents a Field. - * @implements IField + * Constructs a new ReferencedFieldsForType. + * @exports ReferencedFieldsForType + * @classdesc Represents a ReferencedFieldsForType. + * @implements IReferencedFieldsForType * @constructor - * @param {IField=} [properties] Properties to set + * @param {IReferencedFieldsForType=} [properties] Properties to set */ - function Field(properties) { + function ReferencedFieldsForType(properties) { + this.fieldNames = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -6004,283 +5985,91 @@ $root.Field = (function() { } /** - * Field name. - * @member {string} name - * @memberof Field + * ReferencedFieldsForType fieldNames. + * @member {Array.} fieldNames + * @memberof ReferencedFieldsForType * @instance */ - Field.prototype.name = ""; + ReferencedFieldsForType.prototype.fieldNames = $util.emptyArray; /** - * Field returnType. - * @member {string} returnType - * @memberof Field + * ReferencedFieldsForType isInterface. + * @member {boolean} isInterface + * @memberof ReferencedFieldsForType * @instance */ - Field.prototype.returnType = ""; + ReferencedFieldsForType.prototype.isInterface = false; /** - * Creates a new Field instance using the specified properties. + * Creates a new ReferencedFieldsForType instance using the specified properties. * @function create - * @memberof Field + * @memberof ReferencedFieldsForType * @static - * @param {IField=} [properties] Properties to set - * @returns {Field} Field instance + * @param {IReferencedFieldsForType=} [properties] Properties to set + * @returns {ReferencedFieldsForType} ReferencedFieldsForType instance */ - Field.create = function create(properties) { - return new Field(properties); + ReferencedFieldsForType.create = function create(properties) { + return new ReferencedFieldsForType(properties); }; /** - * Encodes the specified Field message. Does not implicitly {@link Field.verify|verify} messages. + * Encodes the specified ReferencedFieldsForType message. Does not implicitly {@link ReferencedFieldsForType.verify|verify} messages. * @function encode - * @memberof Field + * @memberof ReferencedFieldsForType * @static - * @param {IField} message Field message or plain object to encode + * @param {IReferencedFieldsForType} message ReferencedFieldsForType message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - Field.encode = function encode(message, writer) { + ReferencedFieldsForType.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); - if (message.name != null && Object.hasOwnProperty.call(message, "name")) - writer.uint32(/* id 2, wireType 2 =*/18).string(message.name); - if (message.returnType != null && Object.hasOwnProperty.call(message, "returnType")) - writer.uint32(/* id 3, wireType 2 =*/26).string(message.returnType); + if (message.fieldNames != null && message.fieldNames.length) + for (var i = 0; i < message.fieldNames.length; ++i) + writer.uint32(/* id 1, wireType 2 =*/10).string(message.fieldNames[i]); + if (message.isInterface != null && Object.hasOwnProperty.call(message, "isInterface")) + writer.uint32(/* id 2, wireType 0 =*/16).bool(message.isInterface); return writer; }; /** - * Encodes the specified Field message, length delimited. Does not implicitly {@link Field.verify|verify} messages. + * Encodes the specified ReferencedFieldsForType message, length delimited. Does not implicitly {@link ReferencedFieldsForType.verify|verify} messages. * @function encodeDelimited - * @memberof Field + * @memberof ReferencedFieldsForType * @static - * @param {IField} message Field message or plain object to encode + * @param {IReferencedFieldsForType} message ReferencedFieldsForType message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ - Field.encodeDelimited = function encodeDelimited(message, writer) { + ReferencedFieldsForType.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** - * Decodes a Field message from the specified reader or buffer. + * Decodes a ReferencedFieldsForType message from the specified reader or buffer. * @function decode - * @memberof Field + * @memberof ReferencedFieldsForType * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand - * @returns {Field} Field + * @returns {ReferencedFieldsForType} ReferencedFieldsForType * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - Field.decode = function decode(reader, length) { + ReferencedFieldsForType.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.Field(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - case 2: - message.name = reader.string(); - break; - case 3: - message.returnType = reader.string(); - break; - default: - reader.skipType(tag & 7); - break; - } - } - return message; - }; - - /** - * Decodes a Field message from the specified reader or buffer, length delimited. - * @function decodeDelimited - * @memberof Field - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {Field} Field - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - Field.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = new $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a Field message. - * @function verify - * @memberof Field - * @static - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - Field.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (message.name != null && message.hasOwnProperty("name")) - if (!$util.isString(message.name)) - return "name: string expected"; - if (message.returnType != null && message.hasOwnProperty("returnType")) - if (!$util.isString(message.returnType)) - return "returnType: string expected"; - return null; - }; - - /** - * Creates a plain object from a Field message. Also converts values to other types if specified. - * @function toObject - * @memberof Field - * @static - * @param {Field} message Field - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - Field.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - object.name = ""; - object.returnType = ""; - } - if (message.name != null && message.hasOwnProperty("name")) - object.name = message.name; - if (message.returnType != null && message.hasOwnProperty("returnType")) - object.returnType = message.returnType; - return object; - }; - - /** - * Converts this Field to JSON. - * @function toJSON - * @memberof Field - * @instance - * @returns {Object.} JSON object - */ - Field.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - return Field; -})(); - -$root.Type = (function() { - - /** - * Properties of a Type. - * @exports IType - * @interface IType - * @property {string|null} [name] Type name - * @property {Array.|null} [field] Type field - */ - - /** - * Constructs a new Type. - * @exports Type - * @classdesc Represents a Type. - * @implements IType - * @constructor - * @param {IType=} [properties] Properties to set - */ - function Type(properties) { - this.field = []; - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * Type name. - * @member {string} name - * @memberof Type - * @instance - */ - Type.prototype.name = ""; - - /** - * Type field. - * @member {Array.} field - * @memberof Type - * @instance - */ - Type.prototype.field = $util.emptyArray; - - /** - * Creates a new Type instance using the specified properties. - * @function create - * @memberof Type - * @static - * @param {IType=} [properties] Properties to set - * @returns {Type} Type instance - */ - Type.create = function create(properties) { - return new Type(properties); - }; - - /** - * Encodes the specified Type message. Does not implicitly {@link Type.verify|verify} messages. - * @function encode - * @memberof Type - * @static - * @param {IType} message Type message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - Type.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - if (message.name != null && Object.hasOwnProperty.call(message, "name")) - writer.uint32(/* id 1, wireType 2 =*/10).string(message.name); - if (message.field != null && message.field.length) - for (var i = 0; i < message.field.length; ++i) - $root.Field.encode(message.field[i], writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); - return writer; - }; - - /** - * Encodes the specified Type message, length delimited. Does not implicitly {@link Type.verify|verify} messages. - * @function encodeDelimited - * @memberof Type - * @static - * @param {IType} message Type message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - Type.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a Type message from the specified reader or buffer. - * @function decode - * @memberof Type - * @static - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {Type} Type - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - Type.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.Type(); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ReferencedFieldsForType(); while (reader.pos < end) { var tag = reader.uint32(); switch (tag >>> 3) { case 1: - message.name = reader.string(); + if (!(message.fieldNames && message.fieldNames.length)) + message.fieldNames = []; + message.fieldNames.push(reader.string()); break; case 2: - if (!(message.field && message.field.length)) - message.field = []; - message.field.push($root.Field.decode(reader, reader.uint32())); + message.isInterface = reader.bool(); break; default: reader.skipType(tag & 7); @@ -6291,86 +6080,84 @@ $root.Type = (function() { }; /** - * Decodes a Type message from the specified reader or buffer, length delimited. + * Decodes a ReferencedFieldsForType message from the specified reader or buffer, length delimited. * @function decodeDelimited - * @memberof Type + * @memberof ReferencedFieldsForType * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {Type} Type + * @returns {ReferencedFieldsForType} ReferencedFieldsForType * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ - Type.decodeDelimited = function decodeDelimited(reader) { + ReferencedFieldsForType.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** - * Verifies a Type message. + * Verifies a ReferencedFieldsForType message. * @function verify - * @memberof Type + * @memberof ReferencedFieldsForType * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ - Type.verify = function verify(message) { + ReferencedFieldsForType.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; - if (message.name != null && message.hasOwnProperty("name")) - if (!$util.isString(message.name)) - return "name: string expected"; - if (message.field != null && message.hasOwnProperty("field")) { - if (!Array.isArray(message.field)) - return "field: array expected"; - for (var i = 0; i < message.field.length; ++i) { - var error = $root.Field.verify(message.field[i]); - if (error) - return "field." + error; - } + if (message.fieldNames != null && message.hasOwnProperty("fieldNames")) { + if (!Array.isArray(message.fieldNames)) + return "fieldNames: array expected"; + for (var i = 0; i < message.fieldNames.length; ++i) + if (!$util.isString(message.fieldNames[i])) + return "fieldNames: string[] expected"; } + if (message.isInterface != null && message.hasOwnProperty("isInterface")) + if (typeof message.isInterface !== "boolean") + return "isInterface: boolean expected"; return null; }; /** - * Creates a plain object from a Type message. Also converts values to other types if specified. + * Creates a plain object from a ReferencedFieldsForType message. Also converts values to other types if specified. * @function toObject - * @memberof Type + * @memberof ReferencedFieldsForType * @static - * @param {Type} message Type + * @param {ReferencedFieldsForType} message ReferencedFieldsForType * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ - Type.toObject = function toObject(message, options) { + ReferencedFieldsForType.toObject = function toObject(message, options) { if (!options) options = {}; var object = {}; if (options.arrays || options.defaults) - object.field = []; + object.fieldNames = []; if (options.defaults) - object.name = ""; - if (message.name != null && message.hasOwnProperty("name")) - object.name = message.name; - if (message.field && message.field.length) { - object.field = []; - for (var j = 0; j < message.field.length; ++j) - object.field[j] = $root.Field.toObject(message.field[j], options); + object.isInterface = false; + if (message.fieldNames && message.fieldNames.length) { + object.fieldNames = []; + for (var j = 0; j < message.fieldNames.length; ++j) + object.fieldNames[j] = message.fieldNames[j]; } + if (message.isInterface != null && message.hasOwnProperty("isInterface")) + object.isInterface = message.isInterface; return object; }; /** - * Converts this Type to JSON. + * Converts this ReferencedFieldsForType to JSON. * @function toJSON - * @memberof Type + * @memberof ReferencedFieldsForType * @instance * @returns {Object.} JSON object */ - Type.prototype.toJSON = function toJSON() { + ReferencedFieldsForType.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; - return Type; + return ReferencedFieldsForType; })(); $root.Report = (function() { @@ -6853,6 +6640,7 @@ $root.TracesAndStats = (function() { * @interface ITracesAndStats * @property {Array.|null} [trace] TracesAndStats trace * @property {$protobuf.ToArray.|Array.|null} [statsWithContext] TracesAndStats statsWithContext + * @property {Object.|null} [referencedFieldsByType] TracesAndStats referencedFieldsByType * @property {Array.|null} [internalTracesContributingToStats] TracesAndStats internalTracesContributingToStats */ @@ -6867,6 +6655,7 @@ $root.TracesAndStats = (function() { function TracesAndStats(properties) { this.trace = []; this.statsWithContext = []; + this.referencedFieldsByType = {}; this.internalTracesContributingToStats = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) @@ -6890,6 +6679,14 @@ $root.TracesAndStats = (function() { */ TracesAndStats.prototype.statsWithContext = $util.emptyArray; + /** + * TracesAndStats referencedFieldsByType. + * @member {Object.} referencedFieldsByType + * @memberof TracesAndStats + * @instance + */ + TracesAndStats.prototype.referencedFieldsByType = $util.emptyObject; + /** * TracesAndStats internalTracesContributingToStats. * @member {Array.} internalTracesContributingToStats @@ -6944,6 +6741,11 @@ $root.TracesAndStats = (function() { writer.bytes(message.internalTracesContributingToStats[i]); } else $root.Trace.encode(message.internalTracesContributingToStats[i], writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); + if (message.referencedFieldsByType != null && Object.hasOwnProperty.call(message, "referencedFieldsByType")) + for (var keys = Object.keys(message.referencedFieldsByType), i = 0; i < keys.length; ++i) { + writer.uint32(/* id 4, wireType 2 =*/34).fork().uint32(/* id 1, wireType 2 =*/10).string(keys[i]); + $root.ReferencedFieldsForType.encode(message.referencedFieldsByType[keys[i]], writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim().ldelim(); + } return writer; }; @@ -6974,7 +6776,7 @@ $root.TracesAndStats = (function() { TracesAndStats.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.TracesAndStats(); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.TracesAndStats(), key; while (reader.pos < end) { var tag = reader.uint32(); switch (tag >>> 3) { @@ -6988,6 +6790,14 @@ $root.TracesAndStats = (function() { message.statsWithContext = []; message.statsWithContext.push($root.ContextualizedStats.decode(reader, reader.uint32())); break; + case 4: + reader.skip().pos++; + if (message.referencedFieldsByType === $util.emptyObject) + message.referencedFieldsByType = {}; + key = reader.string(); + reader.pos++; + message.referencedFieldsByType[key] = $root.ReferencedFieldsForType.decode(reader, reader.uint32()); + break; case 3: if (!(message.internalTracesContributingToStats && message.internalTracesContributingToStats.length)) message.internalTracesContributingToStats = []; @@ -7052,6 +6862,16 @@ $root.TracesAndStats = (function() { return "statsWithContext." + error; } } + if (message.referencedFieldsByType != null && message.hasOwnProperty("referencedFieldsByType")) { + if (!$util.isObject(message.referencedFieldsByType)) + return "referencedFieldsByType: object expected"; + var key = Object.keys(message.referencedFieldsByType); + for (var i = 0; i < key.length; ++i) { + var error = $root.ReferencedFieldsForType.verify(message.referencedFieldsByType[key[i]]); + if (error) + return "referencedFieldsByType." + error; + } + } if (message.internalTracesContributingToStats != null && message.hasOwnProperty("internalTracesContributingToStats")) { if (!Array.isArray(message.internalTracesContributingToStats)) return "internalTracesContributingToStats: array expected"; @@ -7083,6 +6903,8 @@ $root.TracesAndStats = (function() { object.statsWithContext = []; object.internalTracesContributingToStats = []; } + if (options.objects || options.defaults) + object.referencedFieldsByType = {}; if (message.trace && message.trace.length) { object.trace = []; for (var j = 0; j < message.trace.length; ++j) @@ -7098,6 +6920,12 @@ $root.TracesAndStats = (function() { for (var j = 0; j < message.internalTracesContributingToStats.length; ++j) object.internalTracesContributingToStats[j] = $root.Trace.toObject(message.internalTracesContributingToStats[j], options); } + var keys2; + if (message.referencedFieldsByType && (keys2 = Object.keys(message.referencedFieldsByType)).length) { + object.referencedFieldsByType = {}; + for (var j = 0; j < keys2.length; ++j) + object.referencedFieldsByType[keys2[j]] = $root.ReferencedFieldsForType.toObject(message.referencedFieldsByType[keys2[j]], options); + } return object; }; diff --git a/packages/apollo-reporting-protobuf/generated/reports.proto b/packages/apollo-reporting-protobuf/generated/reports.proto index 05e4d014f11..2e97902fd1c 100644 --- a/packages/apollo-reporting-protobuf/generated/reports.proto +++ b/packages/apollo-reporting-protobuf/generated/reports.proto @@ -202,13 +202,8 @@ message Trace { Details details = 6; - // Note: engineproxy always sets client_name, client_version, and client_address to "none". - // apollo-engine-reporting allows for them to be set by the user. string client_name = 7; string client_version = 8; - string client_address = 9; - // string client_reference_id = 23; - reserved 23; HTTP http = 10; @@ -241,7 +236,8 @@ message Trace { // removed: Node parse = 12; Node validate = 13; // Id128 server_id = 1; Id128 client_id = 2; - reserved 12, 13, 1, 2; + // String client_reference_id = 23; String client_address = 9; + reserved 1, 2, 9, 12, 13, 23; } // The `service` value embedded within the header key is not guaranteed to contain an actual service, @@ -316,8 +312,13 @@ message ContextualizedTypeStats { message FieldStat { string return_type = 3; // required; eg "String!" for User.email:String! + // Number of errors whose path is this field. uint64 errors_count = 4; + // Number of times that the resolver for this field is executed. uint64 count = 5; + // Number of times the resolver for this field is executed that resulted in + // at least one error. "Request" is a misnomer here as this corresponds to + // resolver calls, not overall operations. uint64 requests_with_errors_count = 6; repeated sint64 latency_count = 9 [(js_use_toArray)=true]; // Duration histogram; see docs/histograms.md reserved 1, 2, 7, 8; @@ -329,16 +330,14 @@ message TypeStat { reserved 1, 2; } - -message Field { - string name = 2; // required; eg "email" for User.email:String! - string return_type = 3; // required; eg "String!" for User.email:String! +message ReferencedFieldsForType { + // Contains (eg) "email" for User.email:String! + repeated string field_names = 1; + // True if this type is an interface. + bool is_interface = 2; } -message Type { - string name = 1; // required; eg "User" for User.email:String! - repeated Field field = 2; -} + // This is the top-level message used by the new traces ingress. This // is designed for the apollo-engine-reporting TypeScript agent and will @@ -376,10 +375,18 @@ message ContextualizedStats { } -// A sequence of traces and stats. An individual trace should either be counted as a stat or trace +// A sequence of traces and stats. An individual operation should either be described as a trace +// or as part of stats, but not both. message TracesAndStats { repeated Trace trace = 1 [(js_preEncoded)=true]; repeated ContextualizedStats stats_with_context = 2 [(js_use_toArray)=true]; + // This describes the fields referenced in the operation. Note that this may + // include fields that don't show up in FieldStats (due to being interface fields, + // being nested under null fields or empty lists or non-matching fragments or + // `@include` or `@skip`, etc). It also may be missing fields that show up in FieldStats + // (as FieldStats will include the concrete object type for fields referenced + // via an interface type). + map referenced_fields_by_type = 4; // This field is used to validate that the algorithm used to construct `stats_with_context` // matches similar algorithms in Apollo's servers. It is otherwise ignored and should not // be included in reports. diff --git a/packages/apollo-reporting-protobuf/src/reports.proto b/packages/apollo-reporting-protobuf/src/reports.proto index 05e4d014f11..2e97902fd1c 100644 --- a/packages/apollo-reporting-protobuf/src/reports.proto +++ b/packages/apollo-reporting-protobuf/src/reports.proto @@ -202,13 +202,8 @@ message Trace { Details details = 6; - // Note: engineproxy always sets client_name, client_version, and client_address to "none". - // apollo-engine-reporting allows for them to be set by the user. string client_name = 7; string client_version = 8; - string client_address = 9; - // string client_reference_id = 23; - reserved 23; HTTP http = 10; @@ -241,7 +236,8 @@ message Trace { // removed: Node parse = 12; Node validate = 13; // Id128 server_id = 1; Id128 client_id = 2; - reserved 12, 13, 1, 2; + // String client_reference_id = 23; String client_address = 9; + reserved 1, 2, 9, 12, 13, 23; } // The `service` value embedded within the header key is not guaranteed to contain an actual service, @@ -316,8 +312,13 @@ message ContextualizedTypeStats { message FieldStat { string return_type = 3; // required; eg "String!" for User.email:String! + // Number of errors whose path is this field. uint64 errors_count = 4; + // Number of times that the resolver for this field is executed. uint64 count = 5; + // Number of times the resolver for this field is executed that resulted in + // at least one error. "Request" is a misnomer here as this corresponds to + // resolver calls, not overall operations. uint64 requests_with_errors_count = 6; repeated sint64 latency_count = 9 [(js_use_toArray)=true]; // Duration histogram; see docs/histograms.md reserved 1, 2, 7, 8; @@ -329,16 +330,14 @@ message TypeStat { reserved 1, 2; } - -message Field { - string name = 2; // required; eg "email" for User.email:String! - string return_type = 3; // required; eg "String!" for User.email:String! +message ReferencedFieldsForType { + // Contains (eg) "email" for User.email:String! + repeated string field_names = 1; + // True if this type is an interface. + bool is_interface = 2; } -message Type { - string name = 1; // required; eg "User" for User.email:String! - repeated Field field = 2; -} + // This is the top-level message used by the new traces ingress. This // is designed for the apollo-engine-reporting TypeScript agent and will @@ -376,10 +375,18 @@ message ContextualizedStats { } -// A sequence of traces and stats. An individual trace should either be counted as a stat or trace +// A sequence of traces and stats. An individual operation should either be described as a trace +// or as part of stats, but not both. message TracesAndStats { repeated Trace trace = 1 [(js_preEncoded)=true]; repeated ContextualizedStats stats_with_context = 2 [(js_use_toArray)=true]; + // This describes the fields referenced in the operation. Note that this may + // include fields that don't show up in FieldStats (due to being interface fields, + // being nested under null fields or empty lists or non-matching fragments or + // `@include` or `@skip`, etc). It also may be missing fields that show up in FieldStats + // (as FieldStats will include the concrete object type for fields referenced + // via an interface type). + map referenced_fields_by_type = 4; // This field is used to validate that the algorithm used to construct `stats_with_context` // matches similar algorithms in Apollo's servers. It is otherwise ignored and should not // be included in reports. diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/operationDerivedDataCache.test.ts b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/operationDerivedDataCache.test.ts new file mode 100644 index 00000000000..6b72e9cc1b9 --- /dev/null +++ b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/operationDerivedDataCache.test.ts @@ -0,0 +1,13 @@ +import { operationDerivedDataCacheKey } from '../operationDerivedDataCache'; + +describe('operation-derived data cache key', () => { + it('generates without the operationName', () => { + expect(operationDerivedDataCacheKey('abc123', '')).toEqual('abc123'); + }); + + it('generates with the operationName', () => { + expect(operationDerivedDataCacheKey('abc123', 'myOperation')).toEqual( + 'abc123:myOperation', + ); + }); +}); diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts new file mode 100644 index 00000000000..3cc6448798e --- /dev/null +++ b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts @@ -0,0 +1,299 @@ +import { buildASTSchema, DocumentNode, validate } from 'graphql'; +import gql from 'graphql-tag'; +import { calculateReferencedFieldsByType } from '../referencedFields'; + +const schema = buildASTSchema(gql` + type Query { + f1: Int + f2: Int + a: A + aa: A + myInterface: MyInterface + } + + type A implements MyInterface { + x: ID + y: String! + } + + interface MyInterface { + x: ID + } +`); + +function validateAndCalculate({ + document, + resolvedOperationName = null, +}: { + document: DocumentNode; + resolvedOperationName?: string | null; +}) { + // First validate the document, since calculateReferencedFieldsByType expects + // that. + expect(validate(schema, document)).toStrictEqual([]); + return calculateReferencedFieldsByType({ + schema, + document, + resolvedOperationName, + }); +} + +describe('calculateReferencedFieldsByType', () => { + it('basic', () => { + expect( + validateAndCalculate({ + document: gql` + { + f1 + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "Query": Object { + "fieldNames": Array [ + "f1", + ], + "isInterface": false, + }, + } + `); + }); + + it('aliases use actual field name', () => { + expect( + validateAndCalculate({ + document: gql` + { + aliased: f1 + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "Query": Object { + "fieldNames": Array [ + "f1", + ], + "isInterface": false, + }, + } + `); + }); + + it('multiple operations and fragments', () => { + expect( + validateAndCalculate({ + document: gql` + query Q1 { + f1 + a { + ...AStuff + } + } + query Q2 { + f2 + aa { + ...OtherAStuff + } + } + fragment AStuff on A { + x + } + fragment OtherAStuff on A { + y + } + `, + resolvedOperationName: 'Q1', + }), + ).toMatchInlineSnapshot(` + Object { + "A": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": false, + }, + "Query": Object { + "fieldNames": Array [ + "f1", + "a", + ], + "isInterface": false, + }, + } + `); + }); + + it('interfaces', () => { + expect( + validateAndCalculate({ + document: gql` + query { + myInterface { + x + } + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "MyInterface": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": true, + }, + "Query": Object { + "fieldNames": Array [ + "myInterface", + ], + "isInterface": false, + }, + } + `); + }); + + it('interface with fragment', () => { + expect( + validateAndCalculate({ + document: gql` + query { + myInterface { + x + ... on A { + y + } + } + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "A": Object { + "fieldNames": Array [ + "y", + ], + "isInterface": false, + }, + "MyInterface": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": true, + }, + "Query": Object { + "fieldNames": Array [ + "myInterface", + ], + "isInterface": false, + }, + } + `); + }); +}); + +it('interface with fragment that uses interface field', () => { + expect( + validateAndCalculate({ + document: gql` + query { + myInterface { + ... on A { + # Even though x exists on the interface, we only want this to + # count towards A.x below, because this operation would work just + # as well if x were removed from the interface as long as it was + # left on A. + x + } + } + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "A": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": false, + }, + "Query": Object { + "fieldNames": Array [ + "myInterface", + ], + "isInterface": false, + }, + } + `); +}); + +it('using field both with interface and object should work', () => { + expect( + validateAndCalculate({ + document: gql` + query { + myInterface { + x + ... on A { + x + } + } + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "A": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": false, + }, + "MyInterface": Object { + "fieldNames": Array [ + "x", + ], + "isInterface": true, + }, + "Query": Object { + "fieldNames": Array [ + "myInterface", + ], + "isInterface": false, + }, + } + `); +}); + +it('using field multiple times (same level or otherwise) de-dups', () => { + expect( + validateAndCalculate({ + document: gql` + query { + a1: a { + y + } + a2: a { + y + } + } + `, + }), + ).toMatchInlineSnapshot(` + Object { + "A": Object { + "fieldNames": Array [ + "y", + ], + "isInterface": false, + }, + "Query": Object { + "fieldNames": Array [ + "a", + ], + "isInterface": false, + }, + } + `); +}); diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/signatureCache.test.ts b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/signatureCache.test.ts deleted file mode 100644 index ae2a041f6ee..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/signatureCache.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { signatureCacheKey } from '../signatureCache'; - -describe('signature cache key', () => { - it('generates without the operationName', () => { - expect(signatureCacheKey('abc123', '')).toEqual('abc123'); - }); - - it('generates with the operationName', () => { - expect(signatureCacheKey('abc123', 'myOperation')).toEqual( - 'abc123:myOperation', - ); - }); -}); diff --git a/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts b/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts new file mode 100644 index 00000000000..499100c4b85 --- /dev/null +++ b/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts @@ -0,0 +1,61 @@ +import LRUCache from 'lru-cache'; +import type { Logger } from 'apollo-server-types'; +import type { ReferencedFieldsByType } from './referencedFields'; + +export interface OperationDerivedData { + signature: string; + referencedFieldsByType: ReferencedFieldsByType; +} + +export function createOperationDerivedDataCache({ + logger, +}: { + logger: Logger; +}): LRUCache { + let lastWarn: Date; + let lastDisposals: number = 0; + return new LRUCache({ + // Calculate the length of cache objects by the JSON.stringify byteLength. + length(obj) { + return Buffer.byteLength(JSON.stringify(obj), 'utf8'); + }, + // 10MiB limit, very much approximately since we can't be sure how V8 might + // be storing this data internally. Though this should be enough to store a + // fair amount of operation data, depending on their overall complexity. A + // future version of this might expose some configuration option to grow the + // cache, but ideally, we could do that dynamically based on the resources + // available to the server, and not add more configuration surface area. + // Hopefully the warning message will allow us to evaluate the need with + // more validated input from those that receive it. + max: Math.pow(2, 20) * 10, + dispose() { + // Count the number of disposals between warning messages. + lastDisposals++; + + // Only show a message warning about the high turnover every 60 seconds. + if (!lastWarn || new Date().getTime() - lastWarn.getTime() > 60000) { + // Log the time that we last displayed the message. + lastWarn = new Date(); + logger.warn( + [ + 'This server is processing a high number of unique operations. ', + `A total of ${lastDisposals} records have been `, + 'ejected from the ApolloServerPluginUsageReporting signature cache in the past ', + 'interval. If you see this warning frequently, please open an ', + 'issue on the Apollo Server repository.', + ].join(''), + ); + + // Reset the disposal counter for the next message interval. + lastDisposals = 0; + } + }, + }); +} + +export function operationDerivedDataCacheKey( + queryHash: string, + operationName: string, +) { + return `${queryHash}${operationName && ':' + operationName}`; +} diff --git a/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts b/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts index 02ed5f4a99f..e296d82aea4 100644 --- a/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts +++ b/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts @@ -15,7 +15,11 @@ import { GraphQLRequestContextDidResolveOperation, GraphQLRequestContextWillSendResponse, } from 'apollo-server-types'; -import { createSignatureCache, signatureCacheKey } from './signatureCache'; +import { + createOperationDerivedDataCache, + OperationDerivedData, + operationDerivedDataCacheKey, +} from './operationDerivedDataCache'; import type { ApolloServerPluginUsageReportingOptions, SendValuesBaseOptions, @@ -27,6 +31,11 @@ import { computeCoreSchemaHash } from '../schemaReporting'; import type { InternalApolloServerPlugin } from '../../internalPlugin'; import { OurReport } from './stats'; import { defaultSendOperationsAsTrace } from './defaultSendOperationsAsTrace'; +import { + calculateReferencedFieldsByType, + ReferencedFieldsByType, +} from './referencedFields'; +import type LRUCache from 'lru-cache'; const reportHeaderDefaults = { hostname: os.hostname(), @@ -110,10 +119,15 @@ export function ApolloServerPluginUsageReporting( const sendReportsImmediately = options.sendReportsImmediately ?? serverlessFramework; - // Since calculating the signature for usage reporting is potentially an - // expensive operation, we'll cache the signatures we generate and re-use - // them based on repeated traces for the same `queryHash`. - const signatureCache = createSignatureCache({ logger }); + // Since calculating the signature and referenced fields for usage + // reporting is potentially an expensive operation, we'll cache the data + // we generate and re-use them for repeated operations for the same + // `queryHash`. However, because referenced fields depend on the current + // schema, we want to throw it out entirely any time the schema changes. + let operationDerivedDataCache: { + forSchema: GraphQLSchema; + cache: LRUCache; + } | null = null; const reportDataByExecutableSchemaId: { [executableSchemaId: string]: ReportData | undefined; @@ -561,6 +575,7 @@ export function ApolloServerPluginUsageReporting( const { trace } = treeBuilder; let statsReportKey: string | undefined = undefined; + let referencedFieldsByType: ReferencedFieldsByType; if (!requestContext.document) { statsReportKey = `## GraphQLParseFailure\n`; } else if (graphqlValidationFailure) { @@ -577,11 +592,14 @@ export function ApolloServerPluginUsageReporting( trace.unexecutedOperationName = requestContext.request.operationName || ''; } + referencedFieldsByType = Object.create(null); } else { - const signature = getTraceSignature(); - statsReportKey = `# ${ - requestContext.operationName || '-' - }\n${signature}`; + const operationDerivedData = getOperationDerivedData(); + statsReportKey = `# ${requestContext.operationName || '-'}\n${ + operationDerivedData.signature + }`; + referencedFieldsByType = + operationDerivedData.referencedFieldsByType; } const protobufError = Trace.verify(trace); @@ -596,6 +614,7 @@ export function ApolloServerPluginUsageReporting( graphMightSupportTraces && sendOperationAsTrace(trace, statsReportKey), includeTracesContributingToStats, + referencedFieldsByType, }); // If the buffer gets big (according to our estimate), send. @@ -608,36 +627,61 @@ export function ApolloServerPluginUsageReporting( } } - function getTraceSignature(): string { + // Calculates signature and referenced fields for the current document. + // Only call this when the document properly parses and validates and + // the given operation name (if any) is known! + function getOperationDerivedData(): OperationDerivedData { if (!requestContext.document) { // This shouldn't happen: no document means parse failure, which // uses its own special statsReportKey. throw new Error('No document?'); } - const cacheKey = signatureCacheKey( + const cacheKey = operationDerivedDataCacheKey( requestContext.queryHash, requestContext.operationName || '', ); + // Ensure that the cache we have is for the right schema. + if ( + !operationDerivedDataCache || + operationDerivedDataCache.forSchema !== schema + ) { + operationDerivedDataCache = { + forSchema: schema, + cache: createOperationDerivedDataCache({ logger }), + }; + } + // If we didn't have the signature in the cache, we'll resort to // calculating it. - const cachedSignature = signatureCache.get(cacheKey); - - if (cachedSignature) { - return cachedSignature; + const cachedOperationDerivedData = + operationDerivedDataCache.cache.get(cacheKey); + if (cachedOperationDerivedData) { + return cachedOperationDerivedData; } const generatedSignature = ( options.calculateSignature || defaultUsageReportingSignature )(requestContext.document, requestContext.operationName || ''); + const generatedOperationDerivedData: OperationDerivedData = { + signature: generatedSignature, + referencedFieldsByType: calculateReferencedFieldsByType({ + document: requestContext.document, + schema, + resolvedOperationName: requestContext.operationName ?? null, + }), + }; + // Note that this cache is always an in-memory cache. // If we replace it with a more generic async cache, we should // not await the write operation. - signatureCache.set(cacheKey, generatedSignature); - - return generatedSignature; + operationDerivedDataCache.cache.set( + cacheKey, + generatedOperationDerivedData, + ); + return generatedOperationDerivedData; } }, }; diff --git a/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts b/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts new file mode 100644 index 00000000000..480d5d3052a --- /dev/null +++ b/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts @@ -0,0 +1,82 @@ +import { + DocumentNode, + GraphQLSchema, + isInterfaceType, + separateOperations, + TypeInfo, + visit, + visitWithTypeInfo, +} from 'graphql'; +import { ReferencedFieldsForType } from 'apollo-reporting-protobuf'; + +export type ReferencedFieldsByType = Record; + +export function calculateReferencedFieldsByType({ + document, + schema, + resolvedOperationName, +}: { + document: DocumentNode; + resolvedOperationName: string | null; + schema: GraphQLSchema; +}): ReferencedFieldsByType { + // If the document contains multiple operations, we only care about fields + // referenced in the operation we're using and in fragments that are + // (transitively) spread by that operation. (This is because Studio's field + // usage accounting is all by operation, not by document.) This does mean that + // a field can be textually present in a GraphQL document (and need to exist + // for validation) without being represented in the reported referenced fields + // structure, but we'd need to change the data model of Studio to be based on + // documents rather than fields if we wanted to improve that. + const documentSeparatedByOperation = separateOperations(document); + const filteredDocument = + documentSeparatedByOperation[resolvedOperationName ?? '']; + if (!filteredDocument) { + // This shouldn't happen because we only should call this function on + // properly executable documents. + throw Error( + `shouldn't happen: operation '${resolvedOperationName ?? ''}' not found`, + ); + } + const typeInfo = new TypeInfo(schema); + const interfaces = new Set(); + const referencedFieldSetByType: Record> = Object.create( + null, + ); + visit( + filteredDocument, + visitWithTypeInfo(typeInfo, { + Field(field) { + const fieldName = field.name.value; + const parentType = typeInfo.getParentType(); + if (!parentType) { + throw Error( + `shouldn't happen: missing parent type for field ${fieldName}`, + ); + } + const parentTypeName = parentType.name; + if (!referencedFieldSetByType[parentTypeName]) { + referencedFieldSetByType[parentTypeName] = new Set(); + if (isInterfaceType(parentType)) { + interfaces.add(parentTypeName); + } + } + referencedFieldSetByType[parentTypeName].add(fieldName); + }, + }), + ); + + // Convert from initial representation (which uses Sets to avoid quadratic + // behavior) to the protobufjs objects. (We could also use js_use_toArray here + // but that seems a little overkill.) + const referencedFieldsByType = Object.create(null); + for (const [typeName, fieldNames] of Object.entries( + referencedFieldSetByType, + )) { + referencedFieldsByType[typeName] = new ReferencedFieldsForType({ + fieldNames: [...fieldNames], + isInterface: interfaces.has(typeName), + }); + } + return referencedFieldsByType; +} diff --git a/packages/apollo-server-core/src/plugin/usageReporting/signatureCache.ts b/packages/apollo-server-core/src/plugin/usageReporting/signatureCache.ts deleted file mode 100644 index aed88c85630..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/signatureCache.ts +++ /dev/null @@ -1,56 +0,0 @@ -import LRUCache from 'lru-cache'; -import type { Logger } from 'apollo-server-types'; - -export function createSignatureCache({ - logger, -}: { - logger: Logger; -}): LRUCache { - let lastSignatureCacheWarn: Date; - let lastSignatureCacheDisposals: number = 0; - return new LRUCache({ - // Calculate the length of cache objects by the JSON.stringify byteLength. - length(obj) { - return Buffer.byteLength(JSON.stringify(obj), 'utf8'); - }, - // 3MiB limit, very much approximately since we can't be sure how V8 might - // be storing these strings internally. Though this should be enough to - // store a fair amount of operation signatures (~10000?), depending on their - // overall complexity. A future version of this might expose some - // configuration option to grow the cache, but ideally, we could do that - // dynamically based on the resources available to the server, and not add - // more configuration surface area. Hopefully the warning message will allow - // us to evaluate the need with more validated input from those that receive - // it. - max: Math.pow(2, 20) * 3, - dispose() { - // Count the number of disposals between warning messages. - lastSignatureCacheDisposals++; - - // Only show a message warning about the high turnover every 60 seconds. - if ( - !lastSignatureCacheWarn || - new Date().getTime() - lastSignatureCacheWarn.getTime() > 60000 - ) { - // Log the time that we last displayed the message. - lastSignatureCacheWarn = new Date(); - logger.warn( - [ - 'This server is processing a high number of unique operations. ', - `A total of ${lastSignatureCacheDisposals} records have been `, - 'ejected from the ApolloServerPluginUsageReporting signature cache in the past ', - 'interval. If you see this warning frequently, please open an ', - 'issue on the Apollo Server repository.', - ].join(''), - ); - - // Reset the disposal counter for the next message interval. - lastSignatureCacheDisposals = 0; - } - }, - }); -} - -export function signatureCacheKey(queryHash: string, operationName: string) { - return `${queryHash}${operationName && ':' + operationName}`; -} diff --git a/packages/apollo-server-core/src/plugin/usageReporting/stats.ts b/packages/apollo-server-core/src/plugin/usageReporting/stats.ts index 36917ef7d48..05ecfb7bced 100644 --- a/packages/apollo-server-core/src/plugin/usageReporting/stats.ts +++ b/packages/apollo-server-core/src/plugin/usageReporting/stats.ts @@ -13,6 +13,7 @@ import { IReport, } from 'apollo-reporting-protobuf'; import { iterateOverTrace, ResponseNamePath } from './iterateOverTrace'; +import type { ReferencedFieldsByType } from './referencedFields'; // protobuf.js exports both a class and an interface (starting with I) for each // message type. The class is what it produces when it decodes the message; the @@ -54,13 +55,18 @@ export class OurReport implements Required { trace, asTrace, includeTracesContributingToStats, + referencedFieldsByType, }: { statsReportKey: string; trace: Trace; asTrace: boolean; includeTracesContributingToStats: boolean; + referencedFieldsByType: ReferencedFieldsByType; }) { - const tracesAndStats = this.getTracesAndStats(statsReportKey); + const tracesAndStats = this.getTracesAndStats({ + statsReportKey, + referencedFieldsByType, + }); if (asTrace) { const encodedTrace = Trace.encode(trace).finish(); tracesAndStats.trace.push(encodedTrace); @@ -80,17 +86,47 @@ export class OurReport implements Required { } } - private getTracesAndStats(statsReportKey: string) { + private getTracesAndStats({ + statsReportKey, + referencedFieldsByType, + }: { + statsReportKey: string; + referencedFieldsByType: ReferencedFieldsByType; + }) { const existing = this.tracesPerQuery[statsReportKey]; if (existing) { return existing; } this.sizeEstimator.bytes += estimatedBytesForString(statsReportKey); - return (this.tracesPerQuery[statsReportKey] = new OurTracesAndStats()); + + // Update the size estimator for the referenced field structure. + for (const [typeName, referencedFieldsForType] of Object.entries( + referencedFieldsByType, + )) { + // Two bytes each for the map entry and for the ReferencedFieldsForType, + // and for the isInterface bool if it's set. + this.sizeEstimator.bytes += 2 + 2; + if (referencedFieldsForType.isInterface) { + this.sizeEstimator.bytes += 2; + } + this.sizeEstimator.bytes += estimatedBytesForString(typeName); + for (const fieldName of referencedFieldsForType.fieldNames) { + this.sizeEstimator.bytes += estimatedBytesForString(fieldName); + } + } + + // Include the referenced fields map in the report. (In an ideal world we + // could have a slightly more sophisticated protocol and ingestion pipeline + // that allowed us to only have to send this data once for each + // schema/operation pair.) + return (this.tracesPerQuery[statsReportKey] = new OurTracesAndStats( + referencedFieldsByType, + )); } } class OurTracesAndStats implements Required { + constructor(readonly referencedFieldsByType: ReferencedFieldsByType) {} readonly trace: Uint8Array[] = []; readonly statsWithContext = new StatsByContext(); readonly internalTracesContributingToStats: Uint8Array[] = [];