@faststore/api 1.12.42 → 1.12.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Options as OptionsVTEX } from './platforms/vtex';
2
2
  export * from './__generated__/schema';
3
3
  export * from './platforms/errors';
4
+ export * from './telemetry';
4
5
  export { stringify as stringifyCacheControl } from './directives/cacheControl';
5
6
  export type { CacheControl } from './directives/cacheControl';
6
7
  export declare type Options = OptionsVTEX;
@@ -14,7 +14,7 @@ export interface Options {
14
14
  interface FeatureFlags {
15
15
  enableOrderFormSync?: boolean;
16
16
  }
17
- export interface Context {
17
+ export interface Context extends Pick<Options, 'hideUnavailableItems'> {
18
18
  clients: Clients;
19
19
  loaders: Loaders;
20
20
  /**
@@ -0,0 +1,8 @@
1
+ import type { Options } from '../';
2
+ export declare type GetTelemetryOptions = {
3
+ mode?: 'verbose' | 'dev';
4
+ experimentalSendLogs?: boolean;
5
+ };
6
+ export declare function getTelemetry(APIOptions: Options, telemetryOptions?: GetTelemetryOptions): {
7
+ useFaststoreTelemetry: () => import("@envelop/types").Plugin<import("./useFaststoreTelemetry").PluginContext>;
8
+ };
@@ -0,0 +1,24 @@
1
+ import type { Plugin } from '@envelop/core';
2
+ import { Span } from '@opentelemetry/api';
3
+ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
4
+ import type { LoggerProvider } from '@opentelemetry/sdk-logs';
5
+ export declare enum AttributeName {
6
+ EXECUTION_ERROR = "graphql.error",
7
+ EXECUTION_RESULT = "graphql.result",
8
+ RESOLVER_EXECUTION_ERROR = "graphql.resolver.error",
9
+ RESOLVER_EXCEPTION = "graphql.resolver.exception",
10
+ RESOLVER_FIELD_NAME = "graphql.resolver.fieldName",
11
+ RESOLVER_TYPE_NAME = "graphql.resolver.typeName",
12
+ RESOLVER_RESULT_TYPE = "graphql.resolver.resultType",
13
+ RESOLVER_ARGS = "graphql.resolver.args",
14
+ EXECUTION_OPERATION_NAME = "graphql.operation.name",
15
+ EXECUTION_OPERATION_TYPE = "graphql.operation.type",
16
+ EXECUTION_OPERATION_DOCUMENT = "graphql.document",
17
+ EXECUTION_VARIABLES = "graphql.variables"
18
+ }
19
+ declare const tracingSpanSymbol: unique symbol;
20
+ export declare type PluginContext = {
21
+ [tracingSpanSymbol]: Span;
22
+ };
23
+ export declare const getFaststoreTelemetryPlugin: (tracingProvider: BasicTracerProvider, loggerProvider: LoggerProvider, serviceName: string, experimentalSendLogs: boolean) => (() => Plugin<PluginContext>);
24
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.12.42",
3
+ "version": "1.12.44",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -22,7 +22,12 @@
22
22
  "generate": "graphql-codegen --config codegen.yml"
23
23
  },
24
24
  "dependencies": {
25
+ "@envelop/on-resolve": "^2.0.6",
25
26
  "@graphql-tools/schema": "^8.2.0",
27
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.39.1",
28
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.39.1",
29
+ "@opentelemetry/sdk-logs": "^0.39.1",
30
+ "@opentelemetry/sdk-trace-base": "^1.13.0",
26
31
  "@rollup/plugin-graphql": "^1.0.0",
27
32
  "dataloader": "^2.1.0",
28
33
  "fast-deep-equal": "^3.1.3",
@@ -30,7 +35,8 @@
30
35
  "p-limit": "^3.1.0"
31
36
  },
32
37
  "devDependencies": {
33
- "@faststore/shared": "^1.12.37",
38
+ "@envelop/core": "^2.6.0",
39
+ "@faststore/shared": "^1.12.43",
34
40
  "@graphql-codegen/cli": "2.2.0",
35
41
  "@graphql-codegen/typescript": "2.2.2",
36
42
  "concurrently": "^6.2.1",
@@ -44,7 +50,8 @@
44
50
  "typescript": "^4.4.2"
45
51
  },
46
52
  "peerDependencies": {
53
+ "@envelop/core": "^1 || ^2",
47
54
  "graphql": "^15.6.0"
48
55
  },
49
- "gitHead": "65e4e781d2b4864373e4b50b37511a095c674382"
56
+ "gitHead": "47358f1f317a5fbbf57eaa3ec5d5691095f2264d"
50
57
  }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import type { Options as OptionsVTEX } from './platforms/vtex'
11
11
 
12
12
  export * from './__generated__/schema'
13
13
  export * from './platforms/errors'
14
+ export * from './telemetry'
14
15
  export { stringify as stringifyCacheControl } from './directives/cacheControl'
15
16
  export type { CacheControl } from './directives/cacheControl'
16
17
 
@@ -42,7 +42,7 @@ interface FeatureFlags {
42
42
  enableOrderFormSync?: boolean
43
43
  }
44
44
 
45
- export interface Context {
45
+ export interface Context extends Pick<Options, 'hideUnavailableItems'> {
46
46
  clients: Clients
47
47
  loaders: Loaders
48
48
  /**
@@ -99,6 +99,7 @@ export const getContextFactory =
99
99
  }
100
100
  ctx.clients = getClients(options, ctx)
101
101
  ctx.loaders = getLoaders(options, ctx)
102
+ ctx.hideUnavailableItems = options.hideUnavailableItems
102
103
 
103
104
  return ctx
104
105
  }
@@ -3,6 +3,7 @@ import type { Resolver } from '..'
3
3
  import type { SearchArgs } from '../clients/search'
4
4
  import type { Facet } from '../clients/search/types/FacetSearchResult'
5
5
  import { ProductSearchResult } from '../clients/search/types/ProductSearchResult'
6
+ import { inStock } from '../utils/productStock'
6
7
 
7
8
  type Root = {
8
9
  searchArgs: Omit<SearchArgs, 'type'>
@@ -53,7 +54,7 @@ export const StoreSearchResult: Record<string, Resolver<Root>> = {
53
54
  products: skus,
54
55
  }
55
56
  },
56
- products: async ({ productSearchPromise }) => {
57
+ products: async ({ productSearchPromise }, _, ctx) => {
57
58
  const productSearchResult = await productSearchPromise
58
59
 
59
60
  const skus = productSearchResult.products
@@ -72,10 +73,18 @@ export const StoreSearchResult: Record<string, Resolver<Root>> = {
72
73
  endCursor: productSearchResult.recordsFiltered.toString(),
73
74
  totalCount: productSearchResult.recordsFiltered,
74
75
  },
75
- edges: skus.map((sku, index) => ({
76
- node: sku,
77
- cursor: index.toString(),
78
- })),
76
+ edges: skus
77
+ .filter((sku) => {
78
+ if (ctx.hideUnavailableItems) {
79
+ return sku.sellers.some((item) => inStock(item.commertialOffer))
80
+ } else {
81
+ return true
82
+ }
83
+ }) // TODO: remove this filter when the IS returns correctly with hideUnavailableItems
84
+ .map((sku, index) => ({
85
+ node: sku,
86
+ cursor: index.toString(),
87
+ })),
79
88
  }
80
89
  },
81
90
  facets: async ({ searchArgs }, _, ctx) => {
@@ -0,0 +1,117 @@
1
+ import {
2
+ BasicTracerProvider,
3
+ SimpleSpanProcessor,
4
+ ConsoleSpanExporter,
5
+ } from '@opentelemetry/sdk-trace-base'
6
+ import { Resource } from '@opentelemetry/resources'
7
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
8
+ import {
9
+ LoggerProvider,
10
+ SimpleLogRecordProcessor,
11
+ ConsoleLogRecordExporter,
12
+ } from '@opentelemetry/sdk-logs'
13
+ import { OTLPLogsExporter } from '@opentelemetry/exporter-logs-otlp-grpc'
14
+
15
+ import type { Options } from '../'
16
+ import { getFaststoreTelemetryPlugin } from './useFaststoreTelemetry'
17
+ import packageJson from '../../package.json'
18
+
19
+ export type GetTelemetryOptions = {
20
+ mode?: 'verbose' | 'dev'
21
+ experimentalSendLogs?: boolean
22
+ }
23
+
24
+ const FASTSTORE_API_VERSION = packageJson.version
25
+
26
+ // TODO: These urls are hardcoded for now, but they should be configurable via ENV variables
27
+ // They are only acessible from within the VTEX network, so they are not a security risk
28
+ const TRACE_COLLECTOR_URL = 'opentelemetry-collector.vtex.systems'
29
+ const TRACE_COLLECTOR_URL_DEV = 'opentelemetry-collector-beta.vtex.systems'
30
+ const LOG_COLLECTOR_URL = 'opentelemetry-collector.vtex.systems'
31
+
32
+ export function getTelemetry(
33
+ APIOptions: Options,
34
+ telemetryOptions?: GetTelemetryOptions
35
+ ) {
36
+ const honeycombCollectorOptions = {
37
+ url:
38
+ telemetryOptions?.mode === 'dev'
39
+ ? TRACE_COLLECTOR_URL_DEV
40
+ : TRACE_COLLECTOR_URL,
41
+ }
42
+
43
+ const openSearchCollectorOptions = {
44
+ url: LOG_COLLECTOR_URL,
45
+ }
46
+
47
+ // Create a new tracer provider
48
+ const tracerProvider = new BasicTracerProvider({
49
+ resource: new Resource({
50
+ 'service.name': 'faststore-api',
51
+ 'service.version': FASTSTORE_API_VERSION,
52
+ 'service.name_and_version': `faststore-api@${FASTSTORE_API_VERSION}`,
53
+ platform: APIOptions.platform,
54
+ [`${APIOptions.platform}.account`]: APIOptions.account,
55
+ [`${APIOptions.platform}.environment`]: APIOptions.environment,
56
+ // TODO: include the following properties in the logs
57
+ // [`${APIOptions.platform}.options.hideUnavailableItems`]:
58
+ // APIOptions.hideUnavailableItems,
59
+ // [`${APIOptions.platform}.flags.enableOrderFormSync`]:
60
+ // APIOptions.flags?.enableOrderFormSync,
61
+ // channel: APIOptions.channel,
62
+ locale: APIOptions.locale,
63
+ }),
64
+ })
65
+
66
+ const loggerProvider = new LoggerProvider()
67
+
68
+ // Create trace exporter
69
+ const honeycombExporter = new OTLPTraceExporter(honeycombCollectorOptions)
70
+
71
+ // Create log exporter
72
+ const openSearchExporter = new OTLPLogsExporter(openSearchCollectorOptions)
73
+
74
+ // Set up a span processor to export spans to Honeycomb
75
+ const honeyCombSpanProcessor = new SimpleSpanProcessor(honeycombExporter)
76
+
77
+ // Set up a log record processor to export spans to OpenSearch
78
+ const openSearchLogProcessor = new SimpleLogRecordProcessor(
79
+ openSearchExporter
80
+ )
81
+
82
+ // Register the span processor with the tracer provider
83
+ tracerProvider.addSpanProcessor(honeyCombSpanProcessor)
84
+
85
+ // Register the log record processor with the log provider
86
+ loggerProvider.addLogRecordProcessor(openSearchLogProcessor)
87
+
88
+ if (
89
+ telemetryOptions?.mode === 'verbose' ||
90
+ telemetryOptions?.mode === 'dev'
91
+ ) {
92
+ // Set up a console exporter for verbose mode
93
+ const consoleExporter = new ConsoleSpanExporter()
94
+ const verboseTraceProcessor = new SimpleSpanProcessor(consoleExporter)
95
+
96
+ // Set up processors for verbose mode
97
+ const consoleLogExporter = new ConsoleLogRecordExporter()
98
+ const veboseLogRecordExporter = new SimpleLogRecordProcessor(
99
+ consoleLogExporter
100
+ )
101
+
102
+ tracerProvider.addSpanProcessor(verboseTraceProcessor)
103
+ loggerProvider.addLogRecordProcessor(veboseLogRecordExporter)
104
+ }
105
+
106
+ const useFaststoreTelemetry = getFaststoreTelemetryPlugin(
107
+ // The @opentelemetry/sdk-trace-base was renamed from @opentelemetry/tracing but the
108
+ // envelop plugin doesn't support this change yet. This causes the class type to be incompatible,
109
+ // even if they are the same. https://github.com/n1ru4l/envelop/issues/1610
110
+ tracerProvider as any,
111
+ loggerProvider,
112
+ 'faststore-api',
113
+ telemetryOptions?.experimentalSendLogs ?? false
114
+ )
115
+
116
+ return { useFaststoreTelemetry }
117
+ }
@@ -0,0 +1,256 @@
1
+ import type { Plugin, OnExecuteHookResult } from '@envelop/core'
2
+ import { isAsyncIterable } from '@envelop/core'
3
+ import { useOnResolve } from '@envelop/on-resolve'
4
+ import { SpanKind, Context, Span } from '@opentelemetry/api'
5
+ import {
6
+ trace as openTelTrace,
7
+ context as openTelContext,
8
+ } from '@opentelemetry/api'
9
+ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'
10
+ import type { Path } from 'graphql/jsutils/Path'
11
+ import type { LoggerProvider } from '@opentelemetry/sdk-logs'
12
+ import type { LogRecord } from '@opentelemetry/api-logs'
13
+ import { SeverityNumber } from '@opentelemetry/api-logs'
14
+ import { print, Kind, OperationDefinitionNode } from 'graphql'
15
+
16
+ export enum AttributeName {
17
+ EXECUTION_ERROR = 'graphql.error',
18
+ EXECUTION_RESULT = 'graphql.result',
19
+ RESOLVER_EXECUTION_ERROR = 'graphql.resolver.error',
20
+ RESOLVER_EXCEPTION = 'graphql.resolver.exception',
21
+ RESOLVER_FIELD_NAME = 'graphql.resolver.fieldName',
22
+ RESOLVER_TYPE_NAME = 'graphql.resolver.typeName',
23
+ RESOLVER_RESULT_TYPE = 'graphql.resolver.resultType',
24
+ RESOLVER_ARGS = 'graphql.resolver.args',
25
+ EXECUTION_OPERATION_NAME = 'graphql.operation.name',
26
+ EXECUTION_OPERATION_TYPE = 'graphql.operation.type',
27
+ EXECUTION_OPERATION_DOCUMENT = 'graphql.document',
28
+ EXECUTION_VARIABLES = 'graphql.variables',
29
+ }
30
+
31
+ const tracingSpanSymbol = Symbol('OPEN_TELEMETRY_GRAPHQL')
32
+
33
+ export type PluginContext = {
34
+ [tracingSpanSymbol]: Span
35
+ }
36
+
37
+ function getResolverSpanKey(path: Path) {
38
+ const nodes = []
39
+
40
+ // If the first node (after reversed, it will be the last one) is an integer, that is, identifies a list,
41
+ // we don't want to include it in the key. Note that this will only happen when analysing .prev paths in
42
+ // a list of elements. We just want to remove the initial node that is a integer, not all of them.
43
+ //
44
+ // Nodes with keys 6bc73341b2a183fc::product::image::0::url would not be able to find
45
+ // parents with key 6bc73341b2a183fc::product::image because of the "0" list index -
46
+ // it would search for 6bc73341b2a183fc::product::image::0
47
+ let currentPath: Path | undefined =
48
+ nodes.length === 0 && Number.isInteger(path.key) ? path.prev : path
49
+
50
+ while (currentPath) {
51
+ nodes.push(currentPath.key)
52
+
53
+ currentPath = currentPath.prev
54
+ }
55
+
56
+ return [...nodes].reverse().join('.')
57
+ }
58
+
59
+ export const getFaststoreTelemetryPlugin = (
60
+ tracingProvider: BasicTracerProvider,
61
+ loggerProvider: LoggerProvider,
62
+ serviceName: string,
63
+ experimentalSendLogs: boolean
64
+ ): (() => Plugin<PluginContext>) => {
65
+ return function useFaststoreTelemetry() {
66
+ const tracer = tracingProvider.getTracer(serviceName)
67
+ const logger = loggerProvider.getLogger(serviceName)
68
+
69
+ const resolverContextsByRootSpans: Record<
70
+ string,
71
+ Record<string, Context>
72
+ > = {}
73
+
74
+ return {
75
+ onPluginInit({ addPlugin }) {
76
+ addPlugin(
77
+ // eslint-disable-next-line
78
+ useOnResolve(({ info, context }) => {
79
+ if (
80
+ context &&
81
+ typeof context === 'object' &&
82
+ context[tracingSpanSymbol]
83
+ ) {
84
+ tracer.getActiveSpanProcessor()
85
+ const rootContextSpanId =
86
+ context[tracingSpanSymbol].spanContext().spanId
87
+
88
+ const { fieldName, returnType, parentType, path } = info
89
+
90
+ const previousResolverSpanKey =
91
+ path.prev && getResolverSpanKey(path.prev)
92
+
93
+ let ctx: Context | null = null
94
+
95
+ if (
96
+ previousResolverSpanKey &&
97
+ resolverContextsByRootSpans[rootContextSpanId][
98
+ previousResolverSpanKey
99
+ ]
100
+ ) {
101
+ ctx =
102
+ resolverContextsByRootSpans[rootContextSpanId][
103
+ previousResolverSpanKey
104
+ ]
105
+ } else {
106
+ ctx = openTelTrace.setSpan(
107
+ openTelContext.active(),
108
+ context[tracingSpanSymbol]
109
+ )
110
+
111
+ resolverContextsByRootSpans[rootContextSpanId] =
112
+ resolverContextsByRootSpans[rootContextSpanId] ?? {}
113
+ }
114
+
115
+ const resolverIndexInList = Number.isInteger(path.prev?.key)
116
+ ? `[${path.prev?.key}]`
117
+ : ''
118
+
119
+ const resolverSpan = tracer.startSpan(
120
+ `${parentType.toString()}.${fieldName}${resolverIndexInList}`,
121
+ {
122
+ attributes: {
123
+ [AttributeName.RESOLVER_FIELD_NAME]: fieldName,
124
+ [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(),
125
+ [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(),
126
+ 'meta.span.path': getResolverSpanKey(path),
127
+ },
128
+ },
129
+ ctx
130
+ )
131
+
132
+ const resolverCtx = openTelTrace.setSpan(ctx, resolverSpan)
133
+
134
+ resolverContextsByRootSpans[rootContextSpanId][
135
+ getResolverSpanKey(path)
136
+ ] = resolverCtx
137
+
138
+ return ({ result }) => {
139
+ if (result instanceof Error) {
140
+ resolverSpan.setAttributes({
141
+ error: true,
142
+ 'exception.category':
143
+ AttributeName.RESOLVER_EXECUTION_ERROR,
144
+ 'exception.message': result.message,
145
+ 'exception.type': result.name,
146
+ })
147
+ resolverSpan.recordException(result)
148
+ }
149
+
150
+ resolverSpan.end()
151
+ }
152
+ }
153
+
154
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
155
+ return () => {}
156
+ })
157
+ )
158
+ },
159
+ onExecute({ args, extendContext }) {
160
+ const operationType = args.document.definitions
161
+ .filter((def) => def.kind === Kind.OPERATION_DEFINITION)
162
+ .map((def) => (def as OperationDefinitionNode).operation)?.[0]
163
+
164
+ // Span name according to Semantic Conventions
165
+ // https://github.com/open-telemetry/semantic-conventions
166
+ let spanName = 'GraphQL Operation'
167
+
168
+ if (operationType && args.operationName) {
169
+ spanName = `${operationType} ${args.operationName}`
170
+ } else if (operationType && !args.operationName) {
171
+ spanName = operationType
172
+ }
173
+
174
+ const executionSpan = tracer.startSpan(spanName, {
175
+ kind: SpanKind.SERVER,
176
+ attributes: {
177
+ [AttributeName.EXECUTION_OPERATION_NAME]:
178
+ args.operationName ?? undefined,
179
+ [AttributeName.EXECUTION_OPERATION_TYPE]:
180
+ operationType ?? undefined,
181
+ [AttributeName.EXECUTION_OPERATION_DOCUMENT]: print(args.document),
182
+ },
183
+ })
184
+
185
+ const executeContext = openTelContext.active()
186
+
187
+ const resultCbs: OnExecuteHookResult<PluginContext> = {
188
+ onExecuteDone({ result }) {
189
+ if (isAsyncIterable(result)) {
190
+ executionSpan.end()
191
+ // eslint-disable-next-line no-console
192
+ console.warn(
193
+ `Plugin "newrelic" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.`
194
+ )
195
+
196
+ return
197
+ }
198
+
199
+ const logRecord: LogRecord = {
200
+ context: executeContext,
201
+ attributes: {
202
+ 'service.name': 'faststore-api',
203
+ 'service.version': '1.12.38',
204
+ 'service.name_and_version': 'faststore-api@1.12.38',
205
+ 'vtex.search_index': 'faststore_beta_api',
206
+ [AttributeName.EXECUTION_OPERATION_NAME]:
207
+ args.operationName ?? undefined,
208
+ [AttributeName.EXECUTION_OPERATION_DOCUMENT]: print(
209
+ args.document
210
+ ),
211
+ [AttributeName.EXECUTION_VARIABLES]: JSON.stringify(
212
+ args.variableValues ?? {}
213
+ ),
214
+ },
215
+ }
216
+
217
+ if (
218
+ typeof result.data !== 'undefined' &&
219
+ !(result.errors && result.errors.length > 0)
220
+ ) {
221
+ logRecord.severityNumber = SeverityNumber.INFO
222
+ logRecord.severityText = 'Info'
223
+ logRecord.attributes![AttributeName.EXECUTION_RESULT] =
224
+ JSON.stringify(result)
225
+ }
226
+
227
+ if (result.errors && result.errors.length > 0) {
228
+ logRecord.severityNumber = SeverityNumber.ERROR
229
+ logRecord.severityText = 'Error'
230
+ logRecord.attributes![AttributeName.EXECUTION_ERROR] =
231
+ JSON.stringify(result.errors)
232
+
233
+ executionSpan.setAttributes({
234
+ error: true,
235
+ 'exception.category': AttributeName.EXECUTION_ERROR,
236
+ 'exception.message': JSON.stringify(result.errors),
237
+ })
238
+ }
239
+
240
+ if (experimentalSendLogs) {
241
+ logger.emit(logRecord)
242
+ }
243
+
244
+ executionSpan.end()
245
+ },
246
+ }
247
+
248
+ extendContext({
249
+ [tracingSpanSymbol]: executionSpan,
250
+ })
251
+
252
+ return resultCbs
253
+ },
254
+ }
255
+ }
256
+ }