Skip to content

Commit

Permalink
feat: Log provider (#157)
Browse files Browse the repository at this point in the history
* feat: Log provider received in header

* fix: Tests

* chore: Enhance tests

* chore: Update api

* chore: Add tests
  • Loading branch information
fzavalia authored Mar 22, 2023
1 parent 87022f9 commit 6a1ce5e
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 78 deletions.
124 changes: 65 additions & 59 deletions etc/thegraph-component.api.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,65 @@
## API Report File for "@well-known-components/thegraph-component"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { IConfigComponent } from '@well-known-components/interfaces';
import { IFetchComponent } from '@well-known-components/interfaces';
import { ILoggerComponent } from '@well-known-components/interfaces';
import { IMetricsComponent } from '@well-known-components/interfaces';

// @public
export function createSubgraphComponent(components: createSubgraphComponent.NeededComponents, url: string): Promise<ISubgraphComponent>;

// @public (undocumented)
export namespace createSubgraphComponent {
// (undocumented)
export type NeededComponents = {
logs: ILoggerComponent;
config: IConfigComponent;
fetch: IFetchComponent;
metrics: IMetricsComponent<keyof typeof metricDeclarations>;
};
}

// @public (undocumented)
type Error_2 = {
message: string;
};
export { Error_2 as Error }

// @public (undocumented)
export interface ISubgraphComponent {
query: <T>(query: string, variables?: Variables, remainingAttempts?: number) => Promise<T>;
}

// @public (undocumented)
export namespace ISubgraphComponent {
// (undocumented)
export type Composable = {
subgraph: ISubgraphComponent;
};
}

// @public
export const metricDeclarations: IMetricsComponent.MetricsRecordDefinition<string>;

// @public (undocumented)
export type SubgraphResponse<T> = {
data: T;
errors?: Error_2[] | Error_2;
};

// @public (undocumented)
export type Variables = Record<string, string[] | string | number | boolean | undefined>;

// (No @packageDocumentation comment for this package)

```
## API Report File for "@well-known-components/thegraph-component"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { IConfigComponent } from '@well-known-components/interfaces';
import { IFetchComponent } from '@well-known-components/interfaces';
import { ILoggerComponent } from '@well-known-components/interfaces';
import { IMetricsComponent } from '@well-known-components/interfaces';

// @public
export function createSubgraphComponent(components: createSubgraphComponent.NeededComponents, url: string): Promise<ISubgraphComponent>;

// @public (undocumented)
export namespace createSubgraphComponent {
// (undocumented)
export type NeededComponents = {
logs: ILoggerComponent;
config: IConfigComponent;
fetch: IFetchComponent;
metrics: IMetricsComponent<keyof typeof metricDeclarations>;
};
}

// @public (undocumented)
type Error_2 = {
message: string;
};
export { Error_2 as Error }

// @public (undocumented)
export interface ISubgraphComponent {
query: <T>(query: string, variables?: Variables, remainingAttempts?: number) => Promise<T>;
}

// @public (undocumented)
export namespace ISubgraphComponent {
// (undocumented)
export type Composable = {
subgraph: ISubgraphComponent;
};
}

// @public
export const metricDeclarations: IMetricsComponent.MetricsRecordDefinition<string>;

// @public (undocumented)
export type PostQueryResponse<T> = [SubgraphProvider, SubgraphResponse<T>];

// @public (undocumented)
export type SubgraphProvider = string;

// @public (undocumented)
export type SubgraphResponse<T> = {
data: T;
errors?: Error_2[] | Error_2;
};

// @public (undocumented)
export type Variables = Record<string, string[] | string | number | boolean | undefined>;

// (No @packageDocumentation comment for this package)

```
27 changes: 18 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { IConfigComponent, IFetchComponent, ILoggerComponent, IMetricsComponent } from "@well-known-components/interfaces"
import {
IConfigComponent,
IFetchComponent,
ILoggerComponent,
IMetricsComponent,
} from "@well-known-components/interfaces"
import { randomUUID } from "crypto"
import { setTimeout } from "timers/promises"
import { ISubgraphComponent, SubgraphResponse, Variables } from "./types"
import { withTimeout } from "./utils"
import { ISubgraphComponent, PostQueryResponse, SubgraphProvider, SubgraphResponse, Variables } from "./types"
import { UNKNOWN_SUBGRAPH_PROVIDER, withTimeout } from "./utils"

export * from "./types"

Expand Down Expand Up @@ -42,7 +47,7 @@ export async function createSubgraphComponent(
const logData = { queryId, currentAttempt, attempts, timeoutWait, url }

try {
const response = await withTimeout(
const [provider, response] = await withTimeout(
(abortController) => postQuery<T>(query, variables, abortController),
timeoutWait
)
Expand All @@ -52,13 +57,15 @@ export async function createSubgraphComponent(
const hasErrors = errors !== undefined
if (hasErrors) {
const errorMessages = Array.isArray(errors) ? errors.map((error) => error.message) : [errors.message]
throw new Error(`GraphQL Error: Invalid response. Errors:\n- ${errorMessages.join("\n- ")}`)
throw new Error(
`GraphQL Error: Invalid response. Errors:\n- ${errorMessages.join("\n- ")}. Provider: ${provider}`
)
}

const hasInvalidData = !data || Object.keys(data).length === 0
if (hasInvalidData) {
logger.error("Invalid response", { query, variables, response } as any)
throw new Error("GraphQL Error: Invalid response.")
throw new Error(`GraphQL Error: Invalid response. Provider: ${provider}`)
}

metrics.increment("subgraph_ok_total", { url })
Expand All @@ -83,19 +90,21 @@ export async function createSubgraphComponent(
query: string,
variables: Variables,
abortController: AbortController
): Promise<SubgraphResponse<T>> {
): Promise<PostQueryResponse<T>> {
const response = await fetch.fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "User-agent": USER_AGENT },
body: JSON.stringify({ query, variables }),
signal: abortController.signal,
})

const provider = response.headers.get("X-Subgraph-Provider") ?? UNKNOWN_SUBGRAPH_PROVIDER

if (!response.ok) {
throw new Error(`Invalid request. Status: ${response.status}`)
throw new Error(`Invalid request. Status: ${response.status}. Provider: ${provider}.`)
}

return (await response.json()) as SubgraphResponse<T>
return [provider, (await response.json()) as SubgraphResponse<T>]
}

return {
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export type Error = { message: string }
*/
export type SubgraphResponse<T> = { data: T; errors?: Error[] | Error }

/**
* @public
*/
export type SubgraphProvider = string

/**
* @public
*/
export type PostQueryResponse<T> = [SubgraphProvider, SubgraphResponse<T>]

/**
* @public
*/
Expand Down
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { setTimeout } from "timers/promises"
import { SubgraphProvider } from "./types"

export const UNKNOWN_SUBGRAPH_PROVIDER: SubgraphProvider = "UNKNOWN"

export async function withTimeout<T>(
callback: (abortController: AbortController) => Promise<T>,
Expand Down
72 changes: 62 additions & 10 deletions test/component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { randomUUID } from "crypto"
import { setTimeout } from "timers/promises"
import { ISubgraphComponent, SubgraphResponse, Variables } from "../src"
import { createSubgraphComponent } from "../src"
import { UNKNOWN_SUBGRAPH_PROVIDER } from "../src/utils"
import { SUBGRAPH_URL, test } from "./components"

type Response = Awaited<ReturnType<IFetchComponent["fetch"]>>
Expand Down Expand Up @@ -48,11 +49,14 @@ test("subgraph component", function ({ components, stubComponents }) {
someOther: "data",
},
}

response = {
ok: true,
status: 200,
json: async () => okResponseData,
} as Response
headers: new Map(),
} as unknown as Response

variables = { some: "very interesting", variables: ["we have", "here"] }

fetchMock = jest.spyOn(fetch, "fetch").mockImplementationOnce(async () => response)
Expand Down Expand Up @@ -135,14 +139,17 @@ test("subgraph component", function ({ components, stubComponents }) {
response = {
ok: false,
status: 500,
} as Response
headers: new Map(),
} as unknown as Response

fetchMock = jest.spyOn(fetch, "fetch").mockImplementationOnce(async () => response)
})

it("should throw the appropiate error", async () => {
const { subgraph } = components
await expect(subgraph.query("query", {}, 0)).rejects.toThrow(`Invalid request. Status: ${response.status}`)
await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
`Invalid request. Status: ${response.status}. Provider: ${UNKNOWN_SUBGRAPH_PROVIDER}`
)
})

it("should increment the metric", async () => {
Expand All @@ -154,7 +161,21 @@ test("subgraph component", function ({ components, stubComponents }) {
} catch (error) {}

expect(metrics.increment).toHaveBeenCalledWith("subgraph_errors_total", {
url: SUBGRAPH_URL
url: SUBGRAPH_URL,
})
})

describe("and the response has a subgraph provider header", () => {
beforeEach(() => {
response.headers.set("X-Subgraph-Provider", "SubgraphProvider")
})

it("should have the subgraph provider in the error message", async () => {
const { subgraph } = components

await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
`Invalid request. Status: ${response.status}. Provider: SubgraphProvider`
)
})
})

Expand Down Expand Up @@ -201,7 +222,8 @@ test("subgraph component", function ({ components, stubComponents }) {
ok: true,
status: 400,
json: async () => errorResponseData,
} as Response
headers: new Map(),
} as unknown as Response

fetchMock = jest.spyOn(fetch, "fetch").mockImplementationOnce(async () => response)
})
Expand All @@ -215,7 +237,7 @@ test("subgraph component", function ({ components, stubComponents }) {
} catch (error) {}

expect(metrics.increment).toHaveBeenCalledWith("subgraph_errors_total", {
url: SUBGRAPH_URL
url: SUBGRAPH_URL,
})
})

Expand All @@ -229,7 +251,23 @@ test("subgraph component", function ({ components, stubComponents }) {

it("should throw an Invalid Response error", async () => {
const { subgraph } = components
await expect(subgraph.query("query", {}, 0)).rejects.toThrow("GraphQL Error: Invalid response.")
await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
`GraphQL Error: Invalid response. Provider: ${UNKNOWN_SUBGRAPH_PROVIDER}`
)
})

describe("and the response has a subgraph provider header", () => {
beforeEach(() => {
response.headers.set("X-Subgraph-Provider", "SubgraphProvider")
})

it("should have the subgraph provider in the error message", async () => {
const { subgraph } = components

await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
`GraphQL Error: Invalid response. Provider: SubgraphProvider`
)
})
})
})

Expand All @@ -244,9 +282,23 @@ test("subgraph component", function ({ components, stubComponents }) {
it("should throw them all", async () => {
const { subgraph } = components
await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
"GraphQL Error: Invalid response. Errors:\n- some error\n- happened"
`GraphQL Error: Invalid response. Errors:\n- some error\n- happened. Provider: ${UNKNOWN_SUBGRAPH_PROVIDER}`
)
})

describe("and the response has a subgraph provider header", () => {
beforeEach(() => {
response.headers.set("X-Subgraph-Provider", "SubgraphProvider")
})

it("should have the subgraph provider in the error message", async () => {
const { subgraph } = components

await expect(subgraph.query("query", {}, 0)).rejects.toThrow(
`GraphQL Error: Invalid response. Errors:\n- some error\n- happened. Provider: SubgraphProvider`
)
})
})
})

describe("when the retries is supplied", () => {
Expand Down Expand Up @@ -279,7 +331,7 @@ test("subgraph component", function ({ components, stubComponents }) {

expect(metrics.increment).toHaveBeenCalledTimes(retries + 1)
expect(metrics.increment).toHaveBeenCalledWith("subgraph_errors_total", {
url: SUBGRAPH_URL
url: SUBGRAPH_URL,
})
})
})
Expand Down Expand Up @@ -351,7 +403,7 @@ test("subgraph component", function ({ components, stubComponents }) {
} catch (error) {}

expect(metrics.increment).toHaveBeenCalledWith("subgraph_errors_total", {
url: SUBGRAPH_URL
url: SUBGRAPH_URL,
})
})
})
Expand Down

0 comments on commit 6a1ce5e

Please sign in to comment.