diff options
Diffstat (limited to 'src/frontend/utils')
| -rw-r--r-- | src/frontend/utils/Cypress.ts | 7 | ||||
| -rw-r--r-- | src/frontend/utils/Request.ts | 34 | ||||
| -rw-r--r-- | src/frontend/utils/enums/AttributeNames.ts | 6 | ||||
| -rw-r--r-- | src/frontend/utils/enums/CypressFields.ts | 28 | ||||
| -rw-r--r-- | src/frontend/utils/imageLoader.js | 20 | ||||
| -rw-r--r-- | src/frontend/utils/telemetry/FrontendTracer.ts | 72 | ||||
| -rw-r--r-- | src/frontend/utils/telemetry/Instrumentation.js | 41 | ||||
| -rw-r--r-- | src/frontend/utils/telemetry/InstrumentationMiddleware.ts | 40 | ||||
| -rw-r--r-- | src/frontend/utils/telemetry/SessionIdProcessor.ts | 27 |
9 files changed, 275 insertions, 0 deletions
diff --git a/src/frontend/utils/Cypress.ts b/src/frontend/utils/Cypress.ts new file mode 100644 index 0000000..3e673f4 --- /dev/null +++ b/src/frontend/utils/Cypress.ts @@ -0,0 +1,7 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { CypressFields } from './enums/CypressFields'; + +export const getElementByField = (field: CypressFields, context: Cypress.Chainable = cy) => + context.get(`[data-cy="${field}"]`); diff --git a/src/frontend/utils/Request.ts b/src/frontend/utils/Request.ts new file mode 100644 index 0000000..b0ff20e --- /dev/null +++ b/src/frontend/utils/Request.ts @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +interface IRequestParams { + url: string; + body?: object; + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + queryParams?: Record<string, any>; + headers?: Record<string, string>; +} + +const request = async <T>({ + url = '', + method = 'GET', + body, + queryParams = {}, + headers = { + 'content-type': 'application/json', + }, +}: IRequestParams): Promise<T> => { + const response = await fetch(`${url}?${new URLSearchParams(queryParams).toString()}`, { + method, + body: body ? JSON.stringify(body) : undefined, + headers, + }); + + const responseText = await response.text(); + + if (!!responseText) return JSON.parse(responseText); + + return undefined as unknown as T; +}; + +export default request; diff --git a/src/frontend/utils/enums/AttributeNames.ts b/src/frontend/utils/enums/AttributeNames.ts new file mode 100644 index 0000000..e0820ba --- /dev/null +++ b/src/frontend/utils/enums/AttributeNames.ts @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +export enum AttributeNames { + SESSION_ID = 'session.id' +} diff --git a/src/frontend/utils/enums/CypressFields.ts b/src/frontend/utils/enums/CypressFields.ts new file mode 100644 index 0000000..be85b82 --- /dev/null +++ b/src/frontend/utils/enums/CypressFields.ts @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +export enum CypressFields { + Ad = 'ad', + CartDropdown = 'cart-dropdown', + CartDropdownItem = 'cart-dropdown-item', + CartDropdownItemQuantity = 'cart-dropdown-item-quantity', + CartGoToShopping = 'cart-go-to-shopping', + CartIcon = 'cart-icon', + CartItemCount = 'cart-item-count', + CheckoutPlaceOrder = 'checkout-place-order', + CheckoutItem = 'checkout-item', + CurrencySwitcher = 'currency-switcher', + SessionId = 'session-id', + ProductCard = 'product-card', + ProductList = 'product-list', + ProductPrice = 'product-price', + RecommendationList = 'recommendation-list', + HomePage = 'home-page', + ProductDetail = 'product-detail', + HotProducts = 'hot-products', + ProductPicture = 'product-picture', + ProductName = 'product-name', + ProductDescription = 'product-description', + ProductQuantity = 'product-quantity', + ProductAddToCart = 'product-add-to-cart', +} diff --git a/src/frontend/utils/imageLoader.js b/src/frontend/utils/imageLoader.js new file mode 100644 index 0000000..3718b0a --- /dev/null +++ b/src/frontend/utils/imageLoader.js @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +/* + * We connect to image-provider through the envoy proxy, straight from the browser, for this we need to know the current hostname and port. + * During building and serverside rendering, these are undefined so we use some conditionals and default values. + */ +let hostname = "localhost"; +let port = 8080; +let protocol = "http"; + +if (typeof window !== "undefined" && window.location) { + hostname = window.location.hostname; + port = window.location.port ? parseInt(window.location.port, 10) : (window.location.protocol === "https:" ? 443 : 80); + protocol = window.location.protocol.slice(0, -1); // Remove trailing ':' +} + +export default function imageLoader({ src, width, quality }) { + // We pass down the optimisation request to the image-provider service here, without this, nextJs would try to use internal optimiser which is not working with the external image-provider. + return `${protocol}://${hostname}:${port}/${src}?w=${width}&q=${quality || 75}` +} diff --git a/src/frontend/utils/telemetry/FrontendTracer.ts b/src/frontend/utils/telemetry/FrontendTracer.ts new file mode 100644 index 0000000..d52412c --- /dev/null +++ b/src/frontend/utils/telemetry/FrontendTracer.ts @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core'; +import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web'; +import { resourceFromAttributes, detectResources } from '@opentelemetry/resources'; +import { browserDetector } from '@opentelemetry/opentelemetry-browser-detector'; +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { SessionIdProcessor } from './SessionIdProcessor'; + +const { + NEXT_PUBLIC_OTEL_SERVICE_NAME = '', + NEXT_PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = '', + IS_SYNTHETIC_REQUEST = '', +} = typeof window !== 'undefined' ? window.ENV : {}; + +const FrontendTracer = async () => { + const { ZoneContextManager } = await import('@opentelemetry/context-zone'); + + let resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: NEXT_PUBLIC_OTEL_SERVICE_NAME, + }); + const detectedResources = detectResources({detectors: [browserDetector]}); + resource = resource.merge(detectedResources); + + const provider = new WebTracerProvider({ + resource, + spanProcessors: [ + new SessionIdProcessor(), + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: NEXT_PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || 'http://localhost:4318/v1/traces', + }), + { + scheduledDelayMillis: 500, + } + ), + ], + }); + + const contextManager = new ZoneContextManager(); + + provider.register({ + contextManager, + propagator: new CompositePropagator({ + propagators: [ + new W3CBaggagePropagator(), + new W3CTraceContextPropagator()], + }), + }); + + registerInstrumentations({ + tracerProvider: provider, + instrumentations: [ + getWebAutoInstrumentations({ + '@opentelemetry/instrumentation-fetch': { + propagateTraceHeaderCorsUrls: /.*/, + clearTimingResources: true, + applyCustomAttributesOnSpan(span) { + span.setAttribute('app.synthetic_request', IS_SYNTHETIC_REQUEST); + }, + }, + }), + ], + }); +}; + +export default FrontendTracer; diff --git a/src/frontend/utils/telemetry/Instrumentation.js b/src/frontend/utils/telemetry/Instrumentation.js new file mode 100644 index 0000000..39b0b85 --- /dev/null +++ b/src/frontend/utils/telemetry/Instrumentation.js @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +const opentelemetry = require('@opentelemetry/sdk-node'); +const {getNodeAutoInstrumentations} = require('@opentelemetry/auto-instrumentations-node'); +const {OTLPTraceExporter} = require('@opentelemetry/exporter-trace-otlp-grpc'); +const {OTLPMetricExporter} = require('@opentelemetry/exporter-metrics-otlp-grpc'); +const {PeriodicExportingMetricReader} = require('@opentelemetry/sdk-metrics'); +const {alibabaCloudEcsDetector} = require('@opentelemetry/resource-detector-alibaba-cloud'); +const {awsEc2Detector, awsEksDetector} = require('@opentelemetry/resource-detector-aws'); +const {containerDetector} = require('@opentelemetry/resource-detector-container'); +const {gcpDetector} = require('@opentelemetry/resource-detector-gcp'); +const {envDetector, hostDetector, osDetector, processDetector} = require('@opentelemetry/resources'); + +const sdk = new opentelemetry.NodeSDK({ + traceExporter: new OTLPTraceExporter(), + instrumentations: [ + getNodeAutoInstrumentations({ + // disable fs instrumentation to reduce noise + '@opentelemetry/instrumentation-fs': { + enabled: false, + }, + }) + ], + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), + resourceDetectors: [ + containerDetector, + envDetector, + hostDetector, + osDetector, + processDetector, + alibabaCloudEcsDetector, + awsEksDetector, + awsEc2Detector, + gcpDetector, + ], +}); + +sdk.start(); diff --git a/src/frontend/utils/telemetry/InstrumentationMiddleware.ts b/src/frontend/utils/telemetry/InstrumentationMiddleware.ts new file mode 100644 index 0000000..ed389af --- /dev/null +++ b/src/frontend/utils/telemetry/InstrumentationMiddleware.ts @@ -0,0 +1,40 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { NextApiHandler } from 'next'; +import {context, Exception, Span, SpanStatusCode, trace} from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { metrics } from '@opentelemetry/api'; + +const meter = metrics.getMeter('frontend'); +const requestCounter = meter.createCounter('app.frontend.requests'); + +const InstrumentationMiddleware = (handler: NextApiHandler): NextApiHandler => { + return async (request, response) => { + const {method, url = ''} = request; + const [target] = url.split('?'); + + const span = trace.getSpan(context.active()) as Span; + + let httpStatus = 200; + try { + await runWithSpan(span, async () => handler(request, response)); + httpStatus = response.statusCode; + } catch (error) { + span.recordException(error as Exception); + span.setStatus({ code: SpanStatusCode.ERROR }); + httpStatus = 500; + throw error; + } finally { + requestCounter.add(1, { method, target, status: httpStatus }); + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpStatus); + } + }; +}; + +async function runWithSpan(parentSpan: Span, fn: () => Promise<unknown>) { + const ctx = trace.setSpan(context.active(), parentSpan); + return await context.with(ctx, fn); +} + +export default InstrumentationMiddleware; diff --git a/src/frontend/utils/telemetry/SessionIdProcessor.ts b/src/frontend/utils/telemetry/SessionIdProcessor.ts new file mode 100644 index 0000000..cd89c0b --- /dev/null +++ b/src/frontend/utils/telemetry/SessionIdProcessor.ts @@ -0,0 +1,27 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { Context } from "@opentelemetry/api"; +import { ReadableSpan, Span, SpanProcessor } from "@opentelemetry/sdk-trace-web"; +import SessionGateway from "../../gateways/Session.gateway"; +import { AttributeNames } from "../enums/AttributeNames"; + +const { userId } = SessionGateway.getSession(); + +export class SessionIdProcessor implements SpanProcessor { + forceFlush(): Promise<void> { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onStart(span: Span, parentContext: Context): void { + span.setAttribute(AttributeNames.SESSION_ID, userId); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + onEnd(span: ReadableSpan): void {} + + shutdown(): Promise<void> { + return Promise.resolve(); + } +} |
