Skip to content

Commit

Permalink
Usage reporting: report referenced fields in addition to executed fie…
Browse files Browse the repository at this point in the history
…lds (#5956)

In Apollo Studio, the "Fields" page lets you see how often your fields
are *executed* by operations --- ie, how often their resolvers run. It
additionally lets you see which clients and operations ran those
operations.

However, knowing if an operation *executed* a field doesn't tell the
whole story about the relationship between operations and fields.  You
might also be curious to learn if a field was textually referenced in
the operation itself.

It's possible for a field to be referenced without executing. In fact,
there are many reasons this can happen:
- The field is nested under another field which evaluates to null
- The field is nested under another field which evaluates to an empty
  list
- The field is nested under a non-matching fragment
- The field is nested under `@include` or `@skip`

If you're using the Fields page to determine "what are all the
operations that use this field", you probably want to know about these
usages too!

It's also possible for a field to be executed without being referenced.
That's because when we track "executed fields", we are always tracking
the concrete object type that is being executed, not an interface type
it may have been resolved through.  So with schema:

```graphql
interface Animal {
  legs: Int
}
type Dog implements Animal {
  legs: Int
}
type Query {
  myFavoriteAnimal: Animal  # In practice, always returns a Dog
}
```

the operation `{ myFavoriteAnimal { legs } }` "references"
`Animal.legs`, not `Dog.legs`, but it is the field `Dog.legs` that is
executed.

(Additionally, when using federation, fields can be executed without
being referenced in the operation if the query plan requires them to be
executed to fulfill an `@requires` or `@key` directive.)

This PR extends Apollo Server's usage reporting plugin to provide a list
of "referenced fields" along with every operation. Note that these
fields depend only on the operation, not on the variables passed or
anything at execution time, so we don't need one set per trace, just one
set per operation.

In addition to the fact that this statistic is an interesting one, this
will also mean that the Fields page can be useful without needing full
execution tracing. There is a real performance impact of full execution
tracing, especially in the federation context where the ftv1 protocol
involves sending subgraph traces to the gateway/router in-band in the
response.  And not every GraphQL server supports full execution tracing
in the first place.  With support for referenced fields on the Fields
page, you will be able to run a gateway/router in front of an arbitrary
subgraph without federated tracing (whether you want to do this for
performance or lack of implementation reasons) and still get some useful
data on the Fields page.  (You can then perhaps run full tracing on a
sampled subset of queries to get reasonable approximate execution data
too.)

Note that the new Studio functionality may not be in general
availability when this PR merges into the Apollo Server release branch
or when the first alphas are created; it will be fully enabled
(including docs) by the time this PR is released in a non-prerelease.

As part of the implementation, we extend the existing "signature cache"
to cache the referenced field list as well. Note that the signature is a
pure function of the operation, whereas the referenced field list also
depends on the schema, so we add a little mechanism to throw out the
cache if the schema changes.

Fixes #5708.
  • Loading branch information
glasser authored Dec 17, 2021
1 parent e468367 commit 17c6211
Show file tree
Hide file tree
Showing 13 changed files with 764 additions and 544 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
169 changes: 40 additions & 129 deletions packages/apollo-reporting-protobuf/generated/protobuf.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ export interface ITrace {
/** Trace clientVersion */
clientVersion?: (string|null);

/** Trace clientAddress */
clientAddress?: (string|null);

/** Trace http */
http?: (Trace.IHTTP|null);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -2700,6 +2605,9 @@ export interface ITracesAndStats {
/** TracesAndStats statsWithContext */
statsWithContext?: ($protobuf.ToArray<IContextualizedStats>|IContextualizedStats[]|null);

/** TracesAndStats referencedFieldsByType */
referencedFieldsByType?: ({ [k: string]: IReferencedFieldsForType }|null);

/** TracesAndStats internalTracesContributingToStats */
internalTracesContributingToStats?: ((ITrace|Uint8Array)[]|null);
}
Expand All @@ -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)[];

Expand Down
Loading

0 comments on commit 17c6211

Please sign in to comment.