فهرست منبع

feat(telemetry-plugin): Implement telemetry plugin

Michael Bromley 8 ماه پیش
والد
کامیت
d76235f844
100فایلهای تغییر یافته به همراه3062 افزوده شده و 679 حذف شده
  1. 1 1
      .eslintrc.js
  2. 2 1
      .prettierrc
  3. 165 110
      docker-compose.yml
  4. 6 0
      docs/sidebars.js
  5. 1928 209
      package-lock.json
  6. 1 0
      package.json
  7. 2 2
      packages/admin-ui-plugin/src/plugin.ts
  8. 0 1
      packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.ts
  9. 8 10
      packages/core/build/tsconfig.build.json
  10. 42 35
      packages/core/e2e/customer.e2e-spec.ts
  11. 0 1
      packages/core/e2e/default-scheduler-plugin.e2e-spec.ts
  12. 1 1
      packages/core/e2e/list-query-builder.e2e-spec.ts
  13. 0 1
      packages/core/e2e/order.e2e-spec.ts
  14. 2 0
      packages/core/src/api/middleware/asset-interceptor-plugin.ts
  15. 2 2
      packages/core/src/api/resolvers/admin/draft-order.resolver.ts
  16. 1 1
      packages/core/src/api/resolvers/admin/product.resolver.ts
  17. 3 0
      packages/core/src/cache/cache.service.ts
  18. 6 5
      packages/core/src/common/index.ts
  19. 106 0
      packages/core/src/common/instrument-decorator.ts
  20. 2 2
      packages/core/src/config/config.module.ts
  21. 1 0
      packages/core/src/config/config.service.ts
  22. 2 0
      packages/core/src/config/default-config.ts
  23. 8 7
      packages/core/src/config/index.ts
  24. 59 0
      packages/core/src/config/system/instrumentation-strategy.ts
  25. 7 0
      packages/core/src/config/system/noop-instrumentation-strategy.ts
  26. 3 1
      packages/core/src/config/vendure-config.ts
  27. 2 0
      packages/core/src/event-bus/event-bus.ts
  28. 15 15
      packages/core/src/index.ts
  29. 8 1
      packages/core/src/job-queue/job-queue.service.ts
  30. 3 6
      packages/core/src/job-queue/job-queue.ts
  31. 2 0
      packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts
  32. 1 1
      packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts
  33. 1 1
      packages/core/src/plugin/default-scheduler-plugin/default-scheduler.plugin.ts
  34. 7 7
      packages/core/src/plugin/index.ts
  35. 2 2
      packages/core/src/scheduler/index.ts
  36. 3 3
      packages/core/src/scheduler/scheduler.service.ts
  37. 2 0
      packages/core/src/service/helpers/list-query-builder/list-query-builder.ts
  38. 2 0
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  39. 4 2
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  40. 2 0
      packages/core/src/service/services/administrator.service.ts
  41. 3 1
      packages/core/src/service/services/asset.service.ts
  42. 4 2
      packages/core/src/service/services/auth.service.ts
  43. 2 1
      packages/core/src/service/services/channel.service.ts
  44. 2 0
      packages/core/src/service/services/collection.service.ts
  45. 3 2
      packages/core/src/service/services/country.service.ts
  46. 3 1
      packages/core/src/service/services/customer-group.service.ts
  47. 3 2
      packages/core/src/service/services/customer.service.ts
  48. 3 1
      packages/core/src/service/services/facet-value.service.ts
  49. 3 3
      packages/core/src/service/services/facet.service.ts
  50. 4 3
      packages/core/src/service/services/fulfillment.service.ts
  51. 2 0
      packages/core/src/service/services/global-settings.service.ts
  52. 2 1
      packages/core/src/service/services/history.service.ts
  53. 3 1
      packages/core/src/service/services/order-testing.service.ts
  54. 4 2
      packages/core/src/service/services/order.service.ts
  55. 2 0
      packages/core/src/service/services/payment-method.service.ts
  56. 5 4
      packages/core/src/service/services/payment.service.ts
  57. 3 1
      packages/core/src/service/services/product-option-group.service.ts
  58. 3 1
      packages/core/src/service/services/product-option.service.ts
  59. 4 2
      packages/core/src/service/services/product-variant.service.ts
  60. 17 10
      packages/core/src/service/services/product.service.ts
  61. 3 3
      packages/core/src/service/services/promotion.service.ts
  62. 2 0
      packages/core/src/service/services/province.service.ts
  63. 2 0
      packages/core/src/service/services/role.service.ts
  64. 2 1
      packages/core/src/service/services/seller.service.ts
  65. 3 0
      packages/core/src/service/services/session.service.ts
  66. 2 0
      packages/core/src/service/services/shipping-method.service.ts
  67. 2 0
      packages/core/src/service/services/stock-level.service.ts
  68. 2 0
      packages/core/src/service/services/stock-location.service.ts
  69. 3 3
      packages/core/src/service/services/stock-movement.service.ts
  70. 6 1
      packages/core/src/service/services/tag.service.ts
  71. 2 0
      packages/core/src/service/services/tax-category.service.ts
  72. 2 0
      packages/core/src/service/services/tax-rate.service.ts
  73. 2 0
      packages/core/src/service/services/user.service.ts
  74. 2 0
      packages/core/src/service/services/zone.service.ts
  75. 1 1
      packages/create/src/helpers.ts
  76. 0 1
      packages/dashboard/src/app/routes/_authenticated/_orders/orders.graphql.ts
  77. 0 1
      packages/dashboard/src/lib/framework/defaults.ts
  78. 1 2
      packages/dashboard/src/lib/framework/document-introspection/get-document-structure.ts
  79. 4 2
      packages/dashboard/src/lib/framework/registry/registry-types.ts
  80. 1 1
      packages/dashboard/src/lib/graphql/graphql-env.d.ts
  81. 0 1
      packages/dashboard/src/lib/hooks/use-local-format.ts
  82. 3 3
      packages/dashboard/src/lib/index.ts
  83. 2 2
      packages/dashboard/vite/utils/ast-utils.spec.ts
  84. 4 5
      packages/dashboard/vite/utils/schema-generator.ts
  85. 7 3
      packages/dashboard/vite/utils/ui-config.ts
  86. 0 10
      packages/dashboard/vite/vite-plugin-admin-api-schema.ts
  87. 1 19
      packages/dev-server/dev-config.ts
  88. 26 0
      packages/dev-server/instrumentation.ts
  89. 111 0
      packages/graphiql-plugin/e2e/graphiql-plugin.e2e-spec.ts
  90. 4 3
      packages/graphiql-plugin/package.json
  91. 0 1
      packages/graphiql-plugin/src/index.ts
  92. 3 148
      packages/graphiql-plugin/src/plugin.spec.ts
  93. 2 2
      packages/graphiql-plugin/src/plugin.ts
  94. 52 0
      packages/telemetry-plugin/package.json
  95. 74 0
      packages/telemetry-plugin/src/config/default-method-hooks.ts
  96. 63 0
      packages/telemetry-plugin/src/config/otel-instrumentation-strategy.ts
  97. 58 0
      packages/telemetry-plugin/src/config/otel-logger.ts
  98. 1 0
      packages/telemetry-plugin/src/constants.ts
  99. 6 0
      packages/telemetry-plugin/src/index.ts
  100. 116 0
      packages/telemetry-plugin/src/instrumentation.ts

+ 1 - 1
.eslintrc.js

@@ -183,7 +183,7 @@ module.exports = {
         'id-denylist': 'off',
         'id-match': 'off',
         'import/order': [
-            'error',
+            'warn',
             {
                 alphabetize: {
                     caseInsensitive: true,

+ 2 - 1
.prettierrc

@@ -3,5 +3,6 @@
     "trailingComma": "all",
     "printWidth": 110,
     "tabWidth": 4,
-    "arrowParens": "avoid"
+    "arrowParens": "avoid",
+    "plugins": ["prettier-plugin-organize-imports"]
 }

+ 165 - 110
docker-compose.yml

@@ -4,114 +4,169 @@
 version: '3.7'
 name: vendure-monorepo
 services:
-  mariadb:
-    image: 'bitnami/mariadb:latest'
-    container_name: mariadb
-    environment:
-      MARIADB_DATABASE: vendure-dev
-      MARIADB_ROOT_USER: vendure
-      MARIADB_ROOT_PASSWORD: password
-    volumes:
-      - 'mariadb_data:/bitnami'
-    ports:
-      - '3306:3306'
-  mysql_8:
-    image: bitnami/mysql:8.0
-    container_name: mysql-8
-    environment:
-      MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
-      MYSQL_DATABASE: vendure-dev
-      MYSQL_ROOT_USER: vendure
-      MYSQL_ROOT_PASSWORD: password
-    volumes:
-      - 'mysql_data:/bitnami'
-    ports:
-      - '3306:3306'
-  mysql_5:
-    image: bitnami/mysql:5.7
-    container_name: mysql-5.7
-    environment:
-      MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
-      MYSQL_DATABASE: vendure-dev
-      MYSQL_ROOT_USER: vendure
-      MYSQL_ROOT_PASSWORD: password
-    volumes:
-      - 'mysql_data:/bitnami'
-    ports:
-      - '3306:3306'
-  postgres_12:
-    image: postgres:12.3
-    container_name: postgres_12
-    environment:
-      POSTGRES_DB: vendure-dev
-      POSTGRES_USER: vendure
-      POSTGRES_PASSWORD: password
-      PGDATA: /var/lib/postgresql/data
-    volumes:
-      - postgres_12_data:/var/lib/postgresql/data
-    ports:
-      - "5432:5432"
-    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c pg_stat_statements.max=100000 -c max_connections=200
-  postgres_16:
-    image: postgres:16
-    container_name: postgres_16
-    environment:
-      POSTGRES_DB: vendure-dev
-      POSTGRES_USER: vendure
-      POSTGRES_PASSWORD: password
-      PGDATA: /var/lib/postgresql/data
-    volumes:
-      - postgres_16_data:/var/lib/postgresql/data
-    ports:
-      - "5432:5432"
-    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c pg_stat_statements.max=100000 -c max_connections=200
-  # This is the Keycloak service which is used
-  # to test the Keycloak auth strategy
-  keycloak:
-    image: quay.io/keycloak/keycloak
-    ports:
-      - "9000:8080"
-    environment:
-      KEYCLOAK_ADMIN: admin
-      KEYCLOAK_ADMIN_PASSWORD: admin
-    command:
-      - start-dev
-      - --import-realm
-    volumes:
-      - keycloak_data:/opt/keycloak/data
-  elasticsearch:
-    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2
-    container_name: elasticsearch
-    environment:
-      - discovery.type=single-node
-      - bootstrap.memory_lock=true
-      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
-    ulimits:
-      memlock:
-        soft: -1
-        hard: -1
-    volumes:
-      - esdata:/usr/share/elasticsearch/data
-    ports:
-      - 9200:9200
-  redis:
-    image: bitnami/redis:7.4.1
-    hostname: redis
-    container_name: redis
-    environment:
-      - ALLOW_EMPTY_PASSWORD=yes
-    ports:
-      - "6379:6379"
+    mariadb:
+        image: 'bitnami/mariadb:latest'
+        container_name: mariadb
+        environment:
+            MARIADB_DATABASE: vendure-dev
+            MARIADB_ROOT_USER: vendure
+            MARIADB_ROOT_PASSWORD: password
+        volumes:
+            - 'mariadb_data:/bitnami'
+        ports:
+            - '3306:3306'
+    mysql_8:
+        image: bitnami/mysql:8.0
+        container_name: mysql-8
+        environment:
+            MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
+            MYSQL_DATABASE: vendure-dev
+            MYSQL_ROOT_USER: vendure
+            MYSQL_ROOT_PASSWORD: password
+        volumes:
+            - 'mysql_data:/bitnami'
+        ports:
+            - '3306:3306'
+    mysql_5:
+        image: bitnami/mysql:5.7
+        container_name: mysql-5.7
+        environment:
+            MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
+            MYSQL_DATABASE: vendure-dev
+            MYSQL_ROOT_USER: vendure
+            MYSQL_ROOT_PASSWORD: password
+        volumes:
+            - 'mysql_data:/bitnami'
+        ports:
+            - '3306:3306'
+    postgres_12:
+        image: postgres:12.3
+        container_name: postgres_12
+        environment:
+            POSTGRES_DB: vendure-dev
+            POSTGRES_USER: vendure
+            POSTGRES_PASSWORD: password
+            PGDATA: /var/lib/postgresql/data
+        volumes:
+            - postgres_12_data:/var/lib/postgresql/data
+        ports:
+            - '5432:5432'
+        command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c pg_stat_statements.max=100000 -c max_connections=200
+    postgres_16:
+        image: postgres:16
+        container_name: postgres_16
+        environment:
+            POSTGRES_DB: vendure-dev
+            POSTGRES_USER: vendure
+            POSTGRES_PASSWORD: password
+            PGDATA: /var/lib/postgresql/data
+        volumes:
+            - postgres_16_data:/var/lib/postgresql/data
+        ports:
+            - '5432:5432'
+        command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c pg_stat_statements.max=100000 -c max_connections=200
+    # This is the Keycloak service which is used
+    # to test the Keycloak auth strategy
+    keycloak:
+        image: quay.io/keycloak/keycloak
+        ports:
+            - '9000:8080'
+        environment:
+            KEYCLOAK_ADMIN: admin
+            KEYCLOAK_ADMIN_PASSWORD: admin
+        command:
+            - start-dev
+            - --import-realm
+        volumes:
+            - keycloak_data:/opt/keycloak/data
+    elasticsearch:
+        image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2
+        container_name: elasticsearch
+        environment:
+            - discovery.type=single-node
+            - bootstrap.memory_lock=true
+            - 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
+        ulimits:
+            memlock:
+                soft: -1
+                hard: -1
+        volumes:
+            - esdata:/usr/share/elasticsearch/data
+        ports:
+            - 9200:9200
+    redis:
+        image: redis:7.2-alpine
+        hostname: redis
+        environment:
+            - ALLOW_EMPTY_PASSWORD=yes
+        ports:
+            - '6379:6379'
+
+    jaeger:
+        image: jaegertracing/all-in-one:latest
+        container_name: jaeger
+        ports:
+            - '4318:4318' # OTLP HTTP receiver
+            - '16686:16686' # Web UI
+        environment:
+            - COLLECTOR_OTLP_ENABLED=true
+        volumes:
+            - jaeger_data:/badger
+
+    loki:
+        image: grafana/loki:3.4
+        ports:
+            - '3100:3100'
+        networks:
+            - loki
+
+    grafana:
+        environment:
+            - GF_PATHS_PROVISIONING=/etc/grafana/provisioning
+            - GF_AUTH_ANONYMOUS_ENABLED=true
+            - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+            - GF_FEATURE_TOGGLES_ENABLE=alertingSimplifiedRouting,alertingQueryAndExpressionsStepMode
+        entrypoint:
+            - sh
+            - -euc
+            - |
+                mkdir -p /etc/grafana/provisioning/datasources
+                cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
+                apiVersion: 1
+                datasources:
+                - name: Loki
+                type: loki
+                access: proxy 
+                orgId: 1
+                url: http://loki:3100
+                basicAuth: false
+                isDefault: true
+                version: 1
+                editable: false
+                EOF
+                /run.sh
+        image: grafana/grafana:latest
+        ports:
+            - '3200:3000'
+        networks:
+            - loki
+
+networks:
+    loki:
+        driver: bridge
+
 volumes:
-  postgres_16_data:
-    driver: local
-  postgres_12_data:
-    driver: local
-  mariadb_data:
-    driver: local
-  mysql_data:
-    driver: local
-  keycloak_data:
-    driver: local
-  esdata:
-    driver: local
+    postgres_16_data:
+        driver: local
+    postgres_12_data:
+        driver: local
+    mariadb_data:
+        driver: local
+    mysql_data:
+        driver: local
+    keycloak_data:
+        driver: local
+    esdata:
+        driver: local
+    jaeger_data:
+        driver: local

+ 6 - 0
docs/sidebars.js

@@ -296,6 +296,12 @@ const sidebars = {
                     link: { type: 'doc', id: 'reference/core-plugins/stellate-plugin/index' },
                     items: [{ type: 'autogenerated', dirName: 'reference/core-plugins/stellate-plugin' }],
                 },
+                {
+                    type: 'category',
+                    label: 'TelemetryPlugin',
+                    link: { type: 'doc', id: 'reference/core-plugins/telemetry-plugin/index' },
+                    items: [{ type: 'autogenerated', dirName: 'reference/core-plugins/telemetry-plugin' }],
+                },
             ],
         },
         {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1928 - 209
package-lock.json


+ 1 - 0
package.json

@@ -55,6 +55,7 @@
     "lerna": "^8.1.2",
     "lint-staged": "^10.5.4",
     "prettier": "^3.2.5",
+    "prettier-plugin-organize-imports": "^4.1.0",
     "rollup": "^4.18.0",
     "tinybench": "^2.6.0",
     "ts-node": "^10.9.2",

+ 2 - 2
packages/admin-ui-plugin/src/plugin.ts

@@ -26,12 +26,12 @@ import path from 'path';
 import { adminApiExtensions } from './api/api-extensions';
 import { MetricsResolver } from './api/metrics.resolver';
 import {
+    DEFAULT_APP_PATH,
     defaultAvailableLanguages,
+    defaultAvailableLocales,
     defaultLanguage,
     defaultLocale,
-    DEFAULT_APP_PATH,
     loggerCtx,
-    defaultAvailableLocales,
 } from './constants';
 import { MetricsService } from './service/metrics.service';
 

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.ts

@@ -2,7 +2,6 @@ import { Component, Input, OnInit } from '@angular/core';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     DataTableLocationId,
-    LogicalOperator,
     ProductVariantFilterParameter,
     ProductVariantListQueryDocument,
     TypedBaseListComponent,

+ 8 - 10
packages/core/build/tsconfig.build.json

@@ -1,12 +1,10 @@
 {
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "removeComments": false,
-    "outDir": "../dist",
-    "rootDirs": ["../src"]
-  },
-  "files": [
-    "../src/index.ts",
-    "../typings.d.ts"
-  ]
+    "extends": "../tsconfig.json",
+    "compilerOptions": {
+        "removeComments": false,
+        "outDir": "../dist",
+        "rootDirs": ["../src"]
+    },
+    "exclude": ["../package.json"],
+    "files": ["../src/index.ts", "../typings.d.ts"]
 }

+ 42 - 35
packages/core/e2e/customer.e2e-spec.ts

@@ -1,26 +1,18 @@
-import { OnModuleInit } from '@nestjs/common';
 import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
-import {
-    AccountRegistrationEvent,
-    EventBus,
-    EventBusModule,
-    mergeConfig,
-    VendurePlugin,
-} from '@vendure/core';
+import { AccountRegistrationEvent, EventBus } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
-import { vi } from 'vitest';
-import { afterAll, beforeAll, describe, expect, it, Mock } from 'vitest';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import { CUSTOMER_FRAGMENT } from './graphql/fragments';
-import { DeletionResult, ErrorCode } from './graphql/generated-e2e-admin-types';
 import * as Codegen from './graphql/generated-e2e-admin-types';
+import { DeletionResult, ErrorCode } from './graphql/generated-e2e-admin-types';
 import {
     ActiveOrderCustomerFragment,
     AddItemToOrderMutation,
@@ -47,31 +39,11 @@ import { ADD_ITEM_TO_ORDER, SET_CUSTOMER } from './graphql/shop-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
-let sendEmailFn: Mock;
-
-/**
- * This mock plugin simulates an EmailPlugin which would send emails
- * on the registration & password reset events.
- */
-@VendurePlugin({
-    imports: [EventBusModule],
-})
-class TestEmailPlugin implements OnModuleInit {
-    constructor(private eventBus: EventBus) {}
-
-    onModuleInit() {
-        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
-            sendEmailFn?.(event);
-        });
-    }
-}
 
 type CustomerListItem = Codegen.GetCustomerListQuery['customers']['items'][number];
 
 describe('Customer resolver', () => {
-    const { server, adminClient, shopClient } = createTestEnvironment(
-        mergeConfig(testConfig(), { plugins: [TestEmailPlugin] }),
-    );
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
 
     let firstCustomer: CustomerListItem;
     let secondCustomer: CustomerListItem;
@@ -470,7 +442,19 @@ describe('Customer resolver', () => {
 
     describe('creation', () => {
         it('triggers verification event if no password supplied', async () => {
-            sendEmailFn = vi.fn();
+            const sendEmailFn = vi.fn();
+            let resolveFn: () => void;
+            const subscription = server.app
+                .get(EventBus)
+                .ofType(AccountRegistrationEvent)
+                .subscribe(event => {
+                    sendEmailFn(event);
+                    resolveFn?.();
+                });
+            const eventReceived = new Promise<void>(resolve => {
+                resolveFn = resolve;
+            });
+
             const { createCustomer } = await adminClient.query<
                 Codegen.CreateCustomerMutation,
                 Codegen.CreateCustomerMutationVariables
@@ -484,13 +468,31 @@ describe('Customer resolver', () => {
             customerErrorGuard.assertSuccess(createCustomer);
 
             expect(createCustomer.user!.verified).toBe(false);
+
+            // Wait for the event to be received before making assertions
+            await eventReceived;
+
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
             expect(sendEmailFn.mock.calls[0][0] instanceof AccountRegistrationEvent).toBe(true);
             expect(sendEmailFn.mock.calls[0][0].user.identifier).toBe('test1@test.com');
+
+            subscription.unsubscribe();
         });
 
         it('creates a verified Customer', async () => {
-            sendEmailFn = vi.fn();
+            const sendEmailFn = vi.fn();
+            let resolveFn: () => void;
+            const subscription = server.app
+                .get(EventBus)
+                .ofType(AccountRegistrationEvent)
+                .subscribe(event => {
+                    sendEmailFn(event);
+                    resolveFn?.();
+                });
+            const eventReceived = new Promise<void>(resolve => {
+                resolveFn = resolve;
+            });
+
             const { createCustomer } = await adminClient.query<
                 Codegen.CreateCustomerMutation,
                 Codegen.CreateCustomerMutationVariables
@@ -504,8 +506,13 @@ describe('Customer resolver', () => {
             });
             customerErrorGuard.assertSuccess(createCustomer);
 
+            // Wait for the event to be received before making assertions
+            await eventReceived;
+
             expect(createCustomer.user!.verified).toBe(true);
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
+
+            subscription.unsubscribe();
         });
 
         it('return error result when using an existing, non-deleted emailAddress', async () => {

+ 0 - 1
packages/core/e2e/default-scheduler-plugin.e2e-spec.ts

@@ -11,7 +11,6 @@ import {
     GetTasksQuery,
     RunTaskMutation,
     RunTaskMutationVariables,
-    UpdateTagMutationVariables,
     UpdateTaskMutation,
     UpdateTaskMutationVariables,
 } from './graphql/generated-e2e-admin-types';

+ 1 - 1
packages/core/e2e/list-query-builder.e2e-spec.ts

@@ -6,7 +6,7 @@ import path from 'path';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import { ListQueryPlugin } from './fixtures/test-plugins/list-query-plugin';
 import { LanguageCode, SortOrder } from './graphql/generated-e2e-admin-types';

+ 0 - 1
packages/core/e2e/order.e2e-spec.ts

@@ -47,7 +47,6 @@ import {
     PaymentFragment,
     RefundFragment,
     RefundOrderDocument,
-    SettlePaymentDocument,
     SortOrder,
     StockMovementType,
     TransitFulfillmentDocument,

+ 2 - 0
packages/core/src/api/middleware/asset-interceptor-plugin.ts

@@ -1,6 +1,7 @@
 import { ApolloServerPlugin, GraphQLRequestListener, GraphQLServerContext } from '@apollo/server';
 import { DocumentNode, GraphQLNamedType, isUnionType } from 'graphql';
 
+import { Instrument } from '../../common/instrument-decorator';
 import { AssetStorageStrategy } from '../../config/asset-storage-strategy/asset-storage-strategy';
 import { ConfigService } from '../../config/config.service';
 import { GraphqlValueTransformer } from '../common/graphql-value-transformer';
@@ -9,6 +10,7 @@ import { GraphqlValueTransformer } from '../common/graphql-value-transformer';
  * Transforms outputs so that any Asset instances are run through the {@link AssetStorageStrategy.toAbsoluteUrl}
  * method before being returned in the response.
  */
+@Instrument()
 export class AssetInterceptorPlugin implements ApolloServerPlugin {
     private graphqlValueTransformer: GraphqlValueTransformer;
     private readonly toAbsoluteUrl: AssetStorageStrategy['toAbsoluteUrl'] | undefined;

+ 2 - 2
packages/core/src/api/resolvers/admin/draft-order.resolver.ts

@@ -17,14 +17,14 @@ import {
     MutationRemoveDraftOrderLineArgs,
     MutationSetCustomerForDraftOrderArgs,
     MutationSetDraftOrderBillingAddressArgs,
+    MutationSetDraftOrderCustomFieldsArgs,
     MutationSetDraftOrderShippingAddressArgs,
     MutationSetDraftOrderShippingMethodArgs,
-    MutationUnsetDraftOrderShippingAddressArgs,
     MutationUnsetDraftOrderBillingAddressArgs,
+    MutationUnsetDraftOrderShippingAddressArgs,
     Permission,
     QueryEligibleShippingMethodsForDraftOrderArgs,
     ShippingMethodQuote,
-    MutationSetDraftOrderCustomFieldsArgs,
 } from '@vendure/common/lib/generated-types';
 
 import { ErrorResultUnion, isGraphQlErrorResult } from '../../../common/error/error-result';

+ 1 - 1
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -29,8 +29,8 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { ErrorResultUnion } from '../../../common/error/error-result';
 import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
-import { Product } from '../../../entity/product/product.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
+import { Product } from '../../../entity/product/product.entity';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductService } from '../../../service/services/product.service';

+ 3 - 0
packages/core/src/cache/cache.service.ts

@@ -1,6 +1,7 @@
 import { Injectable } from '@nestjs/common';
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
 
+import { Instrument } from '../common';
 import { ConfigService } from '../config/config.service';
 import { Logger } from '../config/index';
 import { CacheStrategy, SetCacheKeyOptions } from '../config/system/cache-strategy';
@@ -18,8 +19,10 @@ import { Cache, CacheConfig } from './cache';
  * @docsCategory cache
  */
 @Injectable()
+@Instrument()
 export class CacheService {
     protected cacheStrategy: CacheStrategy;
+
     constructor(private configService: ConfigService) {
         this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
     }

+ 6 - 5
packages/core/src/common/index.ts

@@ -1,15 +1,16 @@
-export * from './finite-state-machine/finite-state-machine';
-export * from './finite-state-machine/types';
 export * from './async-queue';
 export * from './calculated-decorator';
-export * from './error/errors';
 export * from './error/error-result';
+export * from './error/errors';
 export * from './error/generated-graphql-admin-errors';
+export * from './finite-state-machine/finite-state-machine';
+export * from './finite-state-machine/types';
 export * from './injector';
+export * from './instrument-decorator';
 export * from './permission-definition';
-export * from './ttl-cache';
-export * from './self-refreshing-cache';
 export * from './round-money';
+export * from './self-refreshing-cache';
+export * from './ttl-cache';
 export * from './types/common-types';
 export * from './types/entity-relation-paths';
 export * from './types/injectable-strategy';

+ 106 - 0
packages/core/src/common/instrument-decorator.ts

@@ -0,0 +1,106 @@
+import { getConfig } from '../config/config-helpers';
+
+export const ENABLE_INSTRUMENTATION_ENV_VAR = 'VENDURE_ENABLE_INSTRUMENTATION';
+
+const INSTRUMENTED_CLASS = Symbol('InstrumentedClassTarget');
+
+type Constructor<T = any> = new (...args: any[]) => T;
+
+/**
+ * @description
+ * This decorator is used to apply instrumentation to a class. It is intended to be used in conjunction
+ * with an {@link InstrumentationStrategy} which defines how the instrumentation should be applied.
+ *
+ * In order for the instrumentation to be applied, the `VENDURE_ENABLE_INSTRUMENTATION` environment
+ * variable (exported from the `@vendure/core` package as `ENABLE_INSTRUMENTATION_ENV_VAR`) must be set to `true`.
+ * This is done to avoid the overhead of instrumentation in environments where it is not needed.
+ *
+ * For more information on how instrumentation is used, see docs on the TelemetryPlugin.
+ *
+ * @example
+ * ```ts
+ * import { Instrument } from '\@vendure/core';
+ * import { Injectable } from '\@nestjs/common';
+ *
+ * \@Injectable()
+ * // highlight-next-line
+ * \@Instrument()
+ * export class MyService {
+ *
+ *   // Calls to this method will be instrumented
+ *   myMethod() {
+ *     // ...
+ *   }
+ * }
+ * ```
+ *
+ * @since 3.3.0
+ * @docsCategory telemetry
+ */
+export function Instrument(): ClassDecorator {
+    return function (target: any) {
+        // Since the instrumentation is not "free" (it will wrap all instrumented classes in a
+        // Proxy, which has some overhead), we will only do this if explicitly requested by the
+        // presence of this env var. The `@vendure/telemetry-plugin` package sets this in its configuration,
+        // which will be run before any of the Vendure code is loaded.
+        if (process.env[ENABLE_INSTRUMENTATION_ENV_VAR] == null) {
+            return target;
+        }
+        // Add type guard to ensure target is a constructor
+        if (typeof target !== 'function') {
+            return target;
+        }
+        const InstrumentedClass = class extends (target as Constructor<any>) {
+            constructor(...args: any[]) {
+                // eslint-disable-next-line constructor-super
+                super(...args);
+                const config = getConfig();
+                const { instrumentationStrategy } = config.systemOptions;
+                if (!instrumentationStrategy) {
+                    return this;
+                }
+                // eslint-disable-next-line @typescript-eslint/no-this-alias
+                const instance = this;
+                return new Proxy(this, {
+                    get: (obj, prop) => {
+                        const original = obj[prop as string];
+                        if (typeof original === 'function') {
+                            return function (...methodArgs: any[]) {
+                                const applyOriginalFunction =
+                                    original.constructor.name === 'AsyncFunction'
+                                        ? async () => await original.apply(obj, methodArgs)
+                                        : () => original.apply(obj, methodArgs);
+                                const wrappedMethodArgs = {
+                                    instance,
+                                    target,
+                                    methodName: String(prop),
+                                    args: methodArgs,
+                                    applyOriginalFunction,
+                                };
+                                return instrumentationStrategy.wrapMethod(wrappedMethodArgs);
+                            };
+                        }
+                        return original;
+                    },
+                });
+            }
+        };
+
+        // Set the name property of ProxiedClass to match the target's name
+        Object.defineProperty(InstrumentedClass, 'name', { value: target.name });
+        Object.defineProperty(InstrumentedClass, INSTRUMENTED_CLASS, { value: target });
+
+        return InstrumentedClass;
+    };
+}
+
+/**
+ * @description
+ * This function is used to retrieve the original class of an instrumented class. It is intended for
+ * use in an {@link InstrumentationStrategy} only, and should not generally be used in application code.
+ *
+ * @since 3.3.0
+ */
+export function getInstrumentedClassTarget<T>(input: T): T | undefined {
+    return input[INSTRUMENTED_CLASS as keyof T] as T | undefined;
+}

+ 2 - 2
packages/core/src/config/config.module.ts

@@ -1,6 +1,5 @@
 import { Module, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
 import { ModuleRef } from '@nestjs/core';
-import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 
 import { ConfigurableOperationDef } from '../common/configurable-operation';
 import { Injector } from '../common/injector';
@@ -113,7 +112,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
         const { healthChecks, errorHandlers } = this.configService.systemOptions;
         const { assetImportStrategy } = this.configService.importExportOptions;
         const { refundProcess: refundProcess } = this.configService.paymentOptions;
-        const { cacheStrategy } = this.configService.systemOptions;
+        const { cacheStrategy, instrumentationStrategy } = this.configService.systemOptions;
         const entityIdStrategy = entityIdStrategyCurrent ?? entityIdStrategyDeprecated;
         return [
             ...adminAuthenticationStrategy,
@@ -156,6 +155,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             guestCheckoutStrategy,
             ...refundProcess,
             cacheStrategy,
+            ...(instrumentationStrategy ? [instrumentationStrategy] : []),
             ...orderInterceptors,
             schedulerStrategy,
         ];

+ 1 - 0
packages/core/src/config/config.service.ts

@@ -128,6 +128,7 @@ export class ConfigService implements VendureConfig {
     private getCustomFieldsForAllEntities(): Required<CustomFields> {
         const definedCustomFields = this.activeConfig.customFields;
         const metadataArgsStorage = getMetadataArgsStorage();
+
         // We need to check for any entities which have a "customFields" property but which are not
         // explicitly defined in the customFields config. This is because the customFields object
         // only includes the built-in entities. Any custom entities which have a "customFields"

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -53,6 +53,7 @@ import { defaultShippingCalculator } from './shipping-method/default-shipping-ca
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
 import { DefaultShippingLineAssignmentStrategy } from './shipping-method/default-shipping-line-assignment-strategy';
 import { InMemoryCacheStrategy } from './system/in-memory-cache-strategy';
+import { NoopInstrumentationStrategy } from './system/noop-instrumentation-strategy';
 import { DefaultTaxLineCalculationStrategy } from './tax/default-tax-line-calculation-strategy';
 import { DefaultTaxZoneStrategy } from './tax/default-tax-zone-strategy';
 import { RuntimeVendureConfig } from './vendure-config';
@@ -242,5 +243,6 @@ export const defaultConfig: RuntimeVendureConfig = {
         cacheStrategy: new InMemoryCacheStrategy({ cacheSize: 10_000 }),
         healthChecks: [new TypeORMHealthCheckStrategy()],
         errorHandlers: [],
+        instrumentationStrategy: new NoopInstrumentationStrategy(),
     },
 };

+ 8 - 7
packages/core/src/config/index.ts

@@ -23,20 +23,20 @@ export * from './catalog/product-variant-price-selection-strategy';
 export * from './catalog/product-variant-price-update-strategy';
 export * from './catalog/stock-display-strategy';
 export * from './catalog/stock-location-strategy';
+export * from './config-helpers';
 export * from './config.module';
 export * from './config.service';
-export * from './config-helpers';
 export * from './custom-field/custom-field-types';
 export * from './default-config';
+export * from './entity-metadata/entity-metadata-modifier';
 export * from './entity/auto-increment-id-strategy';
+export * from './entity/bigint-money-strategy';
 export * from './entity/default-money-strategy';
 export * from './entity/entity-duplicator';
 export * from './entity/entity-duplicators/index';
-export * from './entity/bigint-money-strategy';
 export * from './entity/entity-id-strategy';
 export * from './entity/money-strategy';
 export * from './entity/uuid-id-strategy';
-export * from './entity-metadata/entity-metadata-modifier';
 export * from './fulfillment/default-fulfillment-process';
 export * from './fulfillment/fulfillment-handler';
 export * from './fulfillment/fulfillment-process';
@@ -51,11 +51,11 @@ export * from './order/active-order-strategy';
 export * from './order/changed-price-handling-strategy';
 export * from './order/default-active-order-strategy';
 export * from './order/default-changed-price-handling-strategy';
+export * from './order/default-guest-checkout-strategy';
 export * from './order/default-order-placed-strategy';
 export * from './order/default-order-process';
 export * from './order/default-order-seller-strategy';
 export * from './order/default-stock-allocation-strategy';
-export * from './order/default-guest-checkout-strategy';
 export * from './order/guest-checkout-strategy';
 export * from './order/merge-orders-strategy';
 export * from './order/order-by-code-access-strategy';
@@ -77,6 +77,7 @@ export * from './payment/payment-method-eligibility-checker';
 export * from './payment/payment-method-handler';
 export * from './payment/payment-process';
 export * from './promotion';
+export * from './refund/default-refund-process';
 export * from './session-cache/default-session-cache-strategy';
 export * from './session-cache/in-memory-session-cache-strategy';
 export * from './session-cache/noop-session-cache-strategy';
@@ -87,12 +88,12 @@ export * from './shipping-method/default-shipping-line-assignment-strategy';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './shipping-method/shipping-line-assignment-strategy';
-export * from './system/health-check-strategy';
 export * from './system/error-handler-strategy';
+export * from './system/health-check-strategy';
+export * from './system/instrumentation-strategy';
+export * from './tax/address-based-tax-zone-strategy';
 export * from './tax/default-tax-line-calculation-strategy';
 export * from './tax/default-tax-zone-strategy';
-export * from './tax/address-based-tax-zone-strategy';
 export * from './tax/tax-line-calculation-strategy';
 export * from './tax/tax-zone-strategy';
 export * from './vendure-config';
-export * from './refund/default-refund-process';

+ 59 - 0
packages/core/src/config/system/instrumentation-strategy.ts

@@ -0,0 +1,59 @@
+import { Type } from '@vendure/common/lib/shared-types';
+
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+
+/**
+ * @description
+ * The arguments that are passed to the `wrapMethod` method of the
+ * {@link InstrumentationStrategy} interface.
+ *
+ * @docsCategory telemetry
+ * @since 3.3.0
+ */
+export interface WrappedMethodArgs {
+    /**
+     * @description
+     * The instance of the class which is being instrumented.
+     */
+    instance: any;
+    /**
+     * @description
+     * The class which is being instrumented.
+     */
+    target: Type<any>;
+    /**
+     * @description
+     * The name of the method which is being instrumented.
+     */
+    methodName: string;
+    /**
+     * @description
+     * The arguments which are passed to the method.
+     */
+    args: any[];
+    /**
+     * @description
+     * A function which applies the original method and returns the result.
+     * This is used to call the original method after the instrumentation has
+     * been applied.
+     */
+    applyOriginalFunction: () => any | Promise<any>;
+}
+
+/**
+ * @description
+ * This interface is used to define a strategy for instrumenting methods of
+ * classes which are decorated with the {@link Instrument} decorator.
+ *
+ * @docsCategory telemetry
+ * @since 3.3.0
+ */
+export interface InstrumentationStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * When a method of an instrumented class is called, it will be wrapped (by means of
+     * a Proxy) and this method will be called. The `applyOriginalFunction` function
+     * will apply the original method and return the result.
+     */
+    wrapMethod(args: WrappedMethodArgs): any;
+}

+ 7 - 0
packages/core/src/config/system/noop-instrumentation-strategy.ts

@@ -0,0 +1,7 @@
+import { InstrumentationStrategy } from './instrumentation-strategy';
+
+export class NoopInstrumentationStrategy implements InstrumentationStrategy {
+    async wrapMethod() {
+        // no-op
+    }
+}

+ 3 - 1
packages/core/src/config/vendure-config.ts

@@ -27,10 +27,10 @@ import { ProductVariantPriceUpdateStrategy } from './catalog/product-variant-pri
 import { StockDisplayStrategy } from './catalog/stock-display-strategy';
 import { StockLocationStrategy } from './catalog/stock-location-strategy';
 import { CustomFields } from './custom-field/custom-field-types';
+import { EntityMetadataModifier } from './entity-metadata/entity-metadata-modifier';
 import { EntityDuplicator } from './entity/entity-duplicator';
 import { EntityIdStrategy } from './entity/entity-id-strategy';
 import { MoneyStrategy } from './entity/money-strategy';
-import { EntityMetadataModifier } from './entity-metadata/entity-metadata-modifier';
 import { FulfillmentHandler } from './fulfillment/fulfillment-handler';
 import { FulfillmentProcess } from './fulfillment/fulfillment-process';
 import { JobQueueStrategy } from './job-queue/job-queue-strategy';
@@ -60,6 +60,7 @@ import { ShippingLineAssignmentStrategy } from './shipping-method/shipping-line-
 import { CacheStrategy } from './system/cache-strategy';
 import { ErrorHandlerStrategy } from './system/error-handler-strategy';
 import { HealthCheckStrategy } from './system/health-check-strategy';
+import { InstrumentationStrategy } from './system/instrumentation-strategy';
 import { TaxLineCalculationStrategy } from './tax/tax-line-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
 
@@ -1127,6 +1128,7 @@ export interface SystemOptions {
      * @default InMemoryCacheStrategy
      */
     cacheStrategy?: CacheStrategy;
+    instrumentationStrategy?: InstrumentationStrategy;
 }
 
 /**

+ 2 - 0
packages/core/src/event-bus/event-bus.ts

@@ -7,6 +7,7 @@ import { EntityManager } from 'typeorm';
 
 import { RequestContext } from '../api/common/request-context';
 import { TRANSACTION_MANAGER_KEY } from '../common/constants';
+import { Instrument } from '../common/instrument-decorator';
 import { Logger } from '../config/logger/vendure-logger';
 import { TransactionSubscriber, TransactionSubscriberError } from '../connection/transaction-subscriber';
 
@@ -95,6 +96,7 @@ export type BlockingEventHandlerOptions<T extends VendureEvent> = {
  * @docsCategory events
  * */
 @Injectable()
+@Instrument()
 export class EventBus implements OnModuleDestroy {
     private eventStream = new Subject<VendureEvent>();
     private destroy$ = new Subject<void>();

+ 15 - 15
packages/core/src/index.ts

@@ -1,27 +1,27 @@
-export * from './bootstrap';
-export { VENDURE_VERSION } from './version';
-export { generateMigration, revertLastMigration, runMigrations } from './migrate';
+export {
+    AdjustmentType,
+    AssetType,
+    CurrencyCode,
+    LanguageCode,
+    Permission,
+} from '@vendure/common/lib/generated-types';
+export * from '@vendure/common/lib/shared-types';
 export * from './api/index';
+export * from './bootstrap';
 export * from './cache/index';
 export * from './common/index';
 export * from './config/index';
 export * from './connection/index';
+export * from './data-import/index';
+export * from './entity/index';
 export * from './event-bus/index';
 export * from './health-check/index';
+export * from './i18n/index';
 export * from './job-queue/index';
+export { generateMigration, revertLastMigration, runMigrations } from './migrate';
 export * from './plugin/index';
 export * from './process-context/index';
-export * from './entity/index';
-export * from './data-import/index';
+export * from './scheduler/index';
 export * from './service/index';
-export * from './i18n/index';
+export { VENDURE_VERSION } from './version';
 export * from './worker/index';
-export * from './scheduler/index';
-export * from '@vendure/common/lib/shared-types';
-export {
-    Permission,
-    LanguageCode,
-    CurrencyCode,
-    AssetType,
-    AdjustmentType,
-} from '@vendure/common/lib/generated-types';

+ 8 - 1
packages/core/src/job-queue/job-queue.service.ts

@@ -1,6 +1,7 @@
 import { Injectable, OnModuleDestroy } from '@nestjs/common';
 import { JobQueue as GraphQlJobQueue } from '@vendure/common/lib/generated-types';
 
+import { Instrument } from '../common';
 import { ConfigService, JobQueueStrategy, Logger } from '../config';
 
 import { loggerCtx } from './constants';
@@ -46,6 +47,7 @@ import { CreateQueueOptions, JobData } from './types';
  * @docsCategory JobQueue
  */
 @Injectable()
+@Instrument()
 export class JobQueueService implements OnModuleDestroy {
     private queues: Array<JobQueue<any>> = [];
     private hasStarted = false;
@@ -54,7 +56,10 @@ export class JobQueueService implements OnModuleDestroy {
         return this.configService.jobQueueOptions.jobQueueStrategy;
     }
 
-    constructor(private configService: ConfigService, private jobBufferService: JobBufferService) {}
+    constructor(
+        private configService: ConfigService,
+        private jobBufferService: JobBufferService,
+    ) {}
 
     /** @internal */
     onModuleDestroy() {
@@ -74,11 +79,13 @@ export class JobQueueService implements OnModuleDestroy {
         }
         const wrappedProcessFn = this.createWrappedProcessFn(options.process);
         options = { ...options, process: wrappedProcessFn };
+
         const queue = new JobQueue(options, this.jobQueueStrategy, this.jobBufferService);
         if (this.hasStarted && this.shouldStartQueue(queue.name)) {
             await queue.start();
         }
         this.queues.push(queue);
+
         return queue;
     }
 

+ 3 - 6
packages/core/src/job-queue/job-queue.ts

@@ -1,14 +1,10 @@
-import { JobState } from '@vendure/common/lib/generated-types';
-import { Subject, Subscription } from 'rxjs';
-import { throttleTime } from 'rxjs/operators';
-
+import { Instrument } from '../common';
 import { JobQueueStrategy } from '../config';
-import { Logger } from '../config/logger/vendure-logger';
 
 import { Job } from './job';
 import { JobBufferService } from './job-buffer/job-buffer.service';
 import { SubscribableJob } from './subscribable-job';
-import { CreateQueueOptions, JobConfig, JobData, JobOptions } from './types';
+import { CreateQueueOptions, JobData, JobOptions } from './types';
 
 /**
  * @description
@@ -22,6 +18,7 @@ import { CreateQueueOptions, JobConfig, JobData, JobOptions } from './types';
  *
  * @docsCategory JobQueue
  */
+@Instrument()
 export class JobQueue<Data extends JobData<Data> = object> {
     private running = false;
 

+ 2 - 0
packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts

@@ -2,6 +2,7 @@ import { JsonCompatible } from '@vendure/common/lib/shared-types';
 
 import { CacheTtlProvider, DefaultCacheTtlProvider } from '../../cache/cache-ttl-provider';
 import { Injector } from '../../common/injector';
+import { Instrument } from '../../common/instrument-decorator';
 import { ConfigService, Logger } from '../../config/index';
 import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy';
 import { TransactionalConnection } from '../../connection/index';
@@ -17,6 +18,7 @@ import { CacheTag } from './cache-tag.entity';
  * @since 3.1.0
  * @docsCategory cache
  */
+@Instrument()
 export class SqlCacheStrategy implements CacheStrategy {
     protected cacheSize = 10_000;
     protected ttlProvider: CacheTtlProvider;

+ 1 - 1
packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts

@@ -28,7 +28,7 @@ export class DefaultSchedulerStrategy implements SchedulerStrategy {
     private connection: TransactionalConnection;
     private injector: Injector;
     private intervalRef: NodeJS.Timeout | undefined;
-    private tasks: Map<string, { task: ScheduledTask; isRegistered: boolean }> = new Map();
+    private readonly tasks: Map<string, { task: ScheduledTask; isRegistered: boolean }> = new Map();
     private pluginOptions: DefaultSchedulerPluginOptions;
     private runningTasks: ScheduledTask[] = [];
 

+ 1 - 1
packages/core/src/plugin/default-scheduler-plugin/default-scheduler.plugin.ts

@@ -2,9 +2,9 @@ import { PluginCommonModule } from '../../plugin/plugin-common.module';
 import { VendurePlugin } from '../../plugin/vendure-plugin';
 
 import {
+    DEFAULT_MANUAL_TRIGGER_CHECK_INTERVAL,
     DEFAULT_SCHEDULER_PLUGIN_OPTIONS,
     DEFAULT_TIMEOUT,
-    DEFAULT_MANUAL_TRIGGER_CHECK_INTERVAL,
 } from './constants';
 import { DefaultSchedulerStrategy } from './default-scheduler-strategy';
 import { ScheduledTaskRecord } from './scheduled-task-record.entity';

+ 7 - 7
packages/core/src/plugin/index.ts

@@ -1,15 +1,15 @@
-export * from './default-search-plugin/index';
+export * from './default-cache-plugin/default-cache-plugin';
+export * from './default-cache-plugin/sql-cache-strategy';
 export * from './default-job-queue-plugin/default-job-queue-plugin';
 export * from './default-job-queue-plugin/job-record-buffer.entity';
 export * from './default-job-queue-plugin/sql-job-buffer-storage-strategy';
-export * from './default-cache-plugin/default-cache-plugin';
-export * from './default-cache-plugin/sql-cache-strategy';
+export * from './default-job-queue-plugin/types';
 export * from './default-scheduler-plugin/default-scheduler.plugin';
+export * from './default-search-plugin/index';
+export * from './plugin-common.module';
+export * from './plugin-metadata';
+export * from './plugin-utils';
 export * from './redis-cache-plugin/redis-cache-plugin';
 export * from './redis-cache-plugin/redis-cache-strategy';
 export * from './redis-cache-plugin/types';
 export * from './vendure-plugin';
-export * from './plugin-common.module';
-export * from './plugin-utils';
-export * from './plugin-metadata';
-export * from './default-job-queue-plugin/types';

+ 2 - 2
packages/core/src/scheduler/index.ts

@@ -1,4 +1,4 @@
-export * from './tasks/clean-sessions-task';
 export * from './scheduled-task';
-export * from './scheduler.service';
 export * from './scheduler-strategy';
+export * from './scheduler.service';
+export * from './tasks/clean-sessions-task';

+ 3 - 3
packages/core/src/scheduler/scheduler.service.ts

@@ -33,11 +33,11 @@ export interface TaskInfo {
  */
 @Injectable()
 export class SchedulerService implements OnApplicationBootstrap, OnApplicationShutdown {
-    private jobs: Map<string, { task: ScheduledTask; job: Cron }> = new Map();
+    private readonly jobs: Map<string, { task: ScheduledTask; job: Cron }> = new Map();
     private shouldRunTasks = false;
     constructor(
-        private configService: ConfigService,
-        private processContext: ProcessContext,
+        private readonly configService: ConfigService,
+        private readonly processContext: ProcessContext,
     ) {}
 
     onApplicationBootstrap() {

+ 2 - 0
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -21,6 +21,7 @@ import {
     SortParameter,
     UserInputError,
 } from '../../../common';
+import { Instrument } from '../../../common/instrument-decorator';
 import { ConfigService, CustomFields, Logger } from '../../../config';
 import { TransactionalConnection } from '../../../connection';
 import { VendureEntity } from '../../../entity';
@@ -198,6 +199,7 @@ export type ExtendedListQueryOptions<T extends VendureEntity> = {
  * @docsWeight 0
  */
 @Injectable()
+@Instrument()
 export class ListQueryBuilder implements OnApplicationBootstrap {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -7,6 +7,7 @@ import { RequestContext } from '../../../api/common/request-context';
 import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
 import { CacheKey } from '../../../common/constants';
 import { InternalServerError } from '../../../common/error/errors';
+import { Instrument } from '../../../common/instrument-decorator';
 import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { OrderLine, TaxRate } from '../../../entity';
@@ -32,6 +33,7 @@ import { prorate } from './prorate';
  * @docsCategory service-helpers
  */
 @Injectable()
+@Instrument()
 export class OrderCalculator {
     constructor(
         private configService: ConfigService,

+ 4 - 2
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -33,16 +33,17 @@ import {
     NegativeQuantityError,
     OrderLimitError,
 } from '../../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../../common/instrument-decorator';
 import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { CustomFieldConfig } from '../../../config/custom-field/custom-field-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { Order } from '../../../entity/order/order.entity';
-import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { FulfillmentLine } from '../../../entity/order-line-reference/fulfillment-line.entity';
 import { OrderModificationLine } from '../../../entity/order-line-reference/order-modification-line.entity';
+import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
+import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
@@ -80,6 +81,7 @@ import { patchEntity } from '../utils/patch-entity';
  * @docsCategory service-helpers
  */
 @Injectable()
+@Instrument()
 export class OrderModifier {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/administrator.service.ts

@@ -9,6 +9,7 @@ import { In, IsNull } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
+import { Instrument } from '../../common';
 import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
@@ -38,6 +39,7 @@ import { UserService } from './user.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class AdministratorService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/asset.service.ts

@@ -27,6 +27,7 @@ import { camelCase } from 'typeorm/util/StringUtils';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
+import { Instrument } from '../../common';
 import { isGraphQlErrorResult } from '../../common/error/error-result';
 import { ForbiddenError, InternalServerError } from '../../common/error/errors';
 import { MimeTypeError } from '../../common/error/generated-graphql-admin-errors';
@@ -39,8 +40,8 @@ import { Asset } from '../../entity/asset/asset.entity';
 import { OrderableAsset } from '../../entity/asset/orderable-asset.entity';
 import { VendureEntity } from '../../entity/base/base.entity';
 import { Collection } from '../../entity/collection/collection.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { AssetChannelEvent } from '../../event-bus/events/asset-channel-event';
 import { AssetEvent } from '../../event-bus/events/asset-event';
@@ -88,6 +89,7 @@ export interface EntityAssetInput {
  * @docsWeight 0
  */
 @Injectable()
+@Instrument()
 export class AssetService {
     private permittedMimeTypes: Array<{ type: string; subtype: string }> = [];
 

+ 4 - 2
packages/core/src/service/services/auth.service.ts

@@ -6,14 +6,15 @@ import { RequestContext } from '../../api/common/request-context';
 import { InternalServerError } from '../../common/error/errors';
 import { InvalidCredentialsError } from '../../common/error/generated-graphql-admin-errors';
 import {
-    InvalidCredentialsError as ShopInvalidCredentialsError,
     NotVerifiedError,
+    InvalidCredentialsError as ShopInvalidCredentialsError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import {
+    NATIVE_AUTH_STRATEGY_NAME,
     NativeAuthenticationData,
     NativeAuthenticationStrategy,
-    NATIVE_AUTH_STRATEGY_NAME,
 } from '../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -34,6 +35,7 @@ import { SessionService } from './session.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class AuthService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 1
packages/core/src/service/services/channel.service.ts

@@ -23,6 +23,7 @@ import {
     UserInputError,
 } from '../../common/error/errors';
 import { LanguageNotAvailableError } from '../../common/error/generated-graphql-admin-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { createSelfRefreshingCache, SelfRefreshingCache } from '../../common/self-refreshing-cache';
 import { ChannelAware, ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -44,7 +45,6 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { GlobalSettingsService } from './global-settings.service';
-
 /**
  * @description
  * Contains methods relating to {@link Channel} entities.
@@ -52,6 +52,7 @@ import { GlobalSettingsService } from './global-settings.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ChannelService {
     private allChannels: SelfRefreshingCache<Channel[], [RequestContext]>;
 

+ 2 - 0
packages/core/src/service/services/collection.service.ts

@@ -25,6 +25,7 @@ import { In, IsNull } from 'typeorm';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { ForbiddenError, IllegalOperationError, UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -74,6 +75,7 @@ export type ApplyCollectionFiltersJobData = {
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class CollectionService implements OnModuleInit {
     private rootCollection: Translated<Collection> | undefined;
     private applyFiltersQueue: JobQueue<ApplyCollectionFiltersJobData>;

+ 3 - 2
packages/core/src/service/services/country.service.ts

@@ -5,11 +5,12 @@ import {
     DeletionResult,
     UpdateCountryInput,
 } from '@vendure/common/lib/generated-types';
-import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
+import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
@@ -17,7 +18,6 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Address } from '../../entity';
 import { Country } from '../../entity/region/country.entity';
 import { RegionTranslation } from '../../entity/region/region-translation.entity';
-import { Region } from '../../entity/region/region.entity';
 import { EventBus } from '../../event-bus';
 import { CountryEvent } from '../../event-bus/events/country-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
@@ -31,6 +31,7 @@ import { TranslatorService } from '../helpers/translator/translator.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class CountryService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/customer-group.service.ts

@@ -15,10 +15,11 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
+import { Customer } from '../../entity/customer/customer.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { CustomerGroupChangeEvent } from '../../event-bus/events/customer-group-change-event';
 import { CustomerGroupEvent } from '../../event-bus/events/customer-group-event';
@@ -35,6 +36,7 @@ import { HistoryService } from './history.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class CustomerGroupService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 2
packages/core/src/service/services/customer.service.ts

@@ -11,7 +11,6 @@ import {
     CreateCustomerInput,
     CreateCustomerResult,
     CustomerFilterParameter,
-    CustomerListOptions,
     DeletionResponse,
     DeletionResult,
     HistoryEntryType,
@@ -38,6 +37,7 @@ import {
     PasswordResetTokenInvalidError,
     PasswordValidationError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../config/auth/native-authentication-strategy';
@@ -46,8 +46,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Address } from '../../entity/address/address.entity';
 import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
 import { Channel } from '../../entity/channel/channel.entity';
-import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
+import { Customer } from '../../entity/customer/customer.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
 import { Order } from '../../entity/order/order.entity';
 import { User } from '../../entity/user/user.entity';
@@ -78,6 +78,7 @@ import { UserService } from './user.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class CustomerService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/facet-value.service.ts

@@ -11,15 +11,16 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Product, ProductVariant } from '../../entity';
-import { Facet } from '../../entity/facet/facet.entity';
 import { FacetValueTranslation } from '../../entity/facet-value/facet-value-translation.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
+import { Facet } from '../../entity/facet/facet.entity';
 import { EventBus } from '../../event-bus';
 import { FacetValueEvent } from '../../event-bus/events/facet-value-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
@@ -37,6 +38,7 @@ import { ChannelService } from './channel.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class FacetValueService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 3
packages/core/src/service/services/facet.service.ts

@@ -15,15 +15,15 @@ import { In } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
-import { ErrorResultUnion, FacetInUseError, ForbiddenError, UserInputError } from '../../common';
+import { ErrorResultUnion, FacetInUseError, ForbiddenError, Instrument, UserInputError } from '../../common';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
+import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { FacetTranslation } from '../../entity/facet/facet-translation.entity';
 import { Facet } from '../../entity/facet/facet.entity';
-import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { EventBus } from '../../event-bus';
 import { FacetEvent } from '../../event-bus/events/facet-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
@@ -43,6 +43,7 @@ import { RoleService } from './role.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class FacetService {
     constructor(
         private connection: TransactionalConnection,
@@ -293,7 +294,6 @@ export class FacetService {
                 this.channelService.assignToChannels(ctx, FacetValue, value.id, [input.channelId]),
             ),
         ]);
-
         return this.connection
             .findByIdsInChannel(
                 ctx,

+ 4 - 3
packages/core/src/service/services/fulfillment.service.ts

@@ -12,19 +12,19 @@ import {
     FulfillmentStateTransitionError,
     InvalidFulfillmentHandlerError,
 } from '../../common/error/generated-graphql-admin-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
-import { Order } from '../../entity/order/order.entity';
-import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { FulfillmentLine } from '../../entity/order-line-reference/fulfillment-line.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { Order } from '../../entity/order/order.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { FulfillmentEvent } from '../../event-bus/events/fulfillment-event';
 import { FulfillmentStateTransitionEvent } from '../../event-bus/events/fulfillment-state-transition-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
 import { FulfillmentStateMachine } from '../helpers/fulfillment-state-machine/fulfillment-state-machine';
-
 /**
  * @description
  * Contains methods relating to {@link Fulfillment} entities.
@@ -32,6 +32,7 @@ import { FulfillmentStateMachine } from '../helpers/fulfillment-state-machine/fu
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class FulfillmentService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/global-settings.service.ts

@@ -5,6 +5,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
 import { CacheKey } from '../../common/constants';
 import { InternalServerError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { GlobalSettings } from '../../entity/global-settings/global-settings.entity';
@@ -20,6 +21,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class GlobalSettingsService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 1
packages/core/src/service/services/history.service.ts

@@ -10,6 +10,7 @@ import {
 import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { CustomerHistoryEntry } from '../../entity/history-entry/customer-history-entry.entity';
@@ -24,7 +25,6 @@ import { PaymentState } from '../helpers/payment-state-machine/payment-state';
 import { RefundState } from '../helpers/refund-state-machine/refund-state';
 
 import { AdministratorService } from './administrator.service';
-
 export interface CustomerHistoryEntryData {
     [HistoryEntryType.CUSTOMER_REGISTERED]: {
         strategy: string;
@@ -246,6 +246,7 @@ export interface UpdateCustomerHistoryEntryArgs<T extends keyof CustomerHistoryE
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class HistoryService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/order-testing.service.ts

@@ -10,11 +10,12 @@ import {
 import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Order } from '../../entity/order/order.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
@@ -32,6 +33,7 @@ import { TranslatorService } from '../helpers/translator/translator.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class OrderTestingService {
     constructor(
         private connection: TransactionalConnection,

+ 4 - 2
packages/core/src/service/services/order.service.ts

@@ -74,6 +74,7 @@ import {
     PaymentDeclinedError,
     PaymentFailedError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -84,10 +85,10 @@ import { Channel } from '../../entity/channel/channel.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
-import { Order } from '../../entity/order/order.entity';
-import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { FulfillmentLine } from '../../entity/order-line-reference/fulfillment-line.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../entity/order-modification/order-modification.entity';
+import { Order } from '../../entity/order/order.entity';
 import { Payment } from '../../entity/payment/payment.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
@@ -137,6 +138,7 @@ import { StockLevelService } from './stock-level.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class OrderService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/payment-method.service.ts

@@ -16,6 +16,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { ForbiddenError, UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -44,6 +45,7 @@ import { RoleService } from './role.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class PaymentMethodService {
     constructor(
         private connection: TransactionalConnection,

+ 5 - 4
packages/core/src/service/services/payment.service.ts

@@ -12,18 +12,18 @@ import {
     RefundStateTransitionError,
 } from '../../common/error/generated-graphql-admin-errors';
 import { IneligiblePaymentMethodError } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { PaymentMetadata } from '../../common/types/common-types';
 import { idsAreEqual } from '../../common/utils';
 import { Logger } from '../../config/logger/vendure-logger';
 import { PaymentMethodHandler } from '../../config/payment/payment-method-handler';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
-import { Order } from '../../entity/order/order.entity';
-import { OrderLine } from '../../entity/order-line/order-line.entity';
-import { FulfillmentLine } from '../../entity/order-line-reference/fulfillment-line.entity';
 import { RefundLine } from '../../entity/order-line-reference/refund-line.entity';
-import { Payment } from '../../entity/payment/payment.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { Order } from '../../entity/order/order.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
+import { Payment } from '../../entity/payment/payment.entity';
 import { Refund } from '../../entity/refund/refund.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { PaymentStateTransitionEvent } from '../../event-bus/events/payment-state-transition-event';
@@ -41,6 +41,7 @@ import { PaymentMethodService } from './payment-method.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class PaymentService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/product-option-group.service.ts

@@ -9,14 +9,15 @@ import { FindManyOptions, IsNull, Like } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
+import { Instrument } from '../../common/instrument-decorator';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { Logger } from '../../config/logger/vendure-logger';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Product } from '../../entity/product/product.entity';
 import { ProductOptionGroupTranslation } from '../../entity/product-option-group/product-option-group-translation.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus';
 import { ProductOptionGroupEvent } from '../../event-bus/events/product-option-group-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
@@ -32,6 +33,7 @@ import { ProductOptionService } from './product-option.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ProductOptionGroupService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 1
packages/core/src/service/services/product-option.service.ts

@@ -9,13 +9,14 @@ import {
 import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { Logger } from '../../config/logger/vendure-logger';
 import { TransactionalConnection } from '../../connection/transactional-connection';
+import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
-import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { EventBus } from '../../event-bus';
 import { ProductOptionEvent } from '../../event-bus/events/product-option-event';
@@ -30,6 +31,7 @@ import { TranslatorService } from '../helpers/translator/translator.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ProductOptionService {
     constructor(
         private connection: TransactionalConnection,

+ 4 - 2
packages/core/src/service/services/product-variant.service.ts

@@ -19,6 +19,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
 import { ForbiddenError, UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { roundMoney } from '../../common/round-money';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
@@ -35,10 +36,10 @@ import {
     TaxCategory,
 } from '../../entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { ProductVariantChannelEvent } from '../../event-bus/events/product-variant-channel-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
@@ -67,6 +68,7 @@ import { TaxCategoryService } from './tax-category.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ProductVariantService {
     constructor(
         private connection: TransactionalConnection,
@@ -639,7 +641,7 @@ export class ProductVariantService {
                 // We don't save the targetPrice again unless it has been assigned
                 // a different price by the ProductVariantPriceUpdateStrategy.
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                !(idsAreEqual(p.id, targetPrice!.id) && p.price === targetPrice!.price),
+                !(idsAreEqual(p.id, targetPrice.id) && p.price === targetPrice.price),
         );
         if (uniqueAdditionalPricesToUpdate.length) {
             const updatedAdditionalPrices = await this.connection

+ 17 - 10
packages/core/src/service/services/product.service.ts

@@ -5,7 +5,6 @@ import {
     DeletionResponse,
     DeletionResult,
     ProductFilterParameter,
-    ProductListOptions,
     RemoveOptionGroupFromProductResult,
     RemoveProductsFromChannelInput,
     UpdateProductInput,
@@ -19,16 +18,17 @@ import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { ErrorResultUnion } from '../../common/error/error-result';
 import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
 import { ProductOptionInUseError } from '../../common/error/generated-graphql-admin-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Channel } from '../../entity/channel/channel.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
-import { ProductTranslation } from '../../entity/product/product-translation.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { ProductTranslation } from '../../entity/product/product-translation.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { ProductChannelEvent } from '../../event-bus/events/product-channel-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
@@ -52,6 +52,7 @@ import { ProductVariantService } from './product-variant.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ProductService {
     private readonly relations = ['featuredAsset', 'assets', 'channels', 'facetValues', 'facetValues.facet'];
 
@@ -93,6 +94,7 @@ export class ProductService {
             effectiveRelations.push('variants');
             customPropertyMap.sku = 'variants.sku';
         }
+
         return this.listQueryBuilder
             .build(Product, options, {
                 relations: effectiveRelations,
@@ -153,11 +155,11 @@ export class ProductService {
             .andWhere('product.id IN (:...ids)', { ids: productIds })
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .getMany()
-            .then(products =>
-                products.map(product =>
+            .then(products => {
+                return products.map(product =>
                     this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]),
-                ),
-            );
+                );
+            });
     }
 
     /**
@@ -179,9 +181,12 @@ export class ProductService {
                 where: { id: productId },
                 relations: ['facetValues'],
             })
-            .then(variant =>
-                !variant ? [] : variant.facetValues.map(o => this.translator.translate(o, ctx, ['facet'])),
-            );
+            .then(product => {
+                if (!product) {
+                    return [];
+                }
+                return product.facetValues.map(o => this.translator.translate(o, ctx, ['facet']));
+            });
     }
 
     async findOneBySlug(
@@ -236,6 +241,7 @@ export class ProductService {
         await this.customFieldRelationService.updateRelations(ctx, Product, input, product);
         await this.assetService.updateEntityAssets(ctx, product, input);
         await this.eventBus.publish(new ProductEvent(ctx, product, 'created', input));
+
         return assertFound(this.findOne(ctx, product.id));
     }
 
@@ -266,6 +272,7 @@ export class ProductService {
         });
         await this.customFieldRelationService.updateRelations(ctx, Product, input, updatedProduct);
         await this.eventBus.publish(new ProductEvent(ctx, updatedProduct, 'updated', input));
+
         return assertFound(this.findOne(ctx, updatedProduct.id));
     }
 

+ 3 - 3
packages/core/src/service/services/promotion.service.ts

@@ -12,7 +12,6 @@ import {
     UpdatePromotionInput,
     UpdatePromotionResult,
 } from '@vendure/common/lib/generated-types';
-import { omit } from '@vendure/common/lib/omit';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { In, IsNull } from 'typeorm';
@@ -20,13 +19,14 @@ import { In, IsNull } from 'typeorm';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { ErrorResultUnion, JustErrorResults } from '../../common/error/error-result';
-import { IllegalOperationError, UserInputError } from '../../common/error/errors';
+import { UserInputError } from '../../common/error/errors';
 import { MissingConditionsError } from '../../common/error/generated-graphql-admin-errors';
 import {
     CouponCodeExpiredError,
     CouponCodeInvalidError,
     CouponCodeLimitError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -45,7 +45,6 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatorService } from '../helpers/translator/translator.service';
-import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { ChannelService } from './channel.service';
 
@@ -56,6 +55,7 @@ import { ChannelService } from './channel.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class PromotionService {
     availableConditions: PromotionCondition[] = [];
     availableActions: PromotionAction[] = [];

+ 2 - 0
packages/core/src/service/services/province.service.ts

@@ -9,6 +9,7 @@ import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
@@ -29,6 +30,7 @@ import { TranslatorService } from '../helpers/translator/translator.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ProvinceService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/role.service.ts

@@ -25,6 +25,7 @@ import {
     InternalServerError,
     UserInputError,
 } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -50,6 +51,7 @@ import { ChannelService } from './channel.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class RoleService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 1
packages/core/src/service/services/seller.service.ts

@@ -8,6 +8,7 @@ import {
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -16,7 +17,6 @@ import { EventBus, SellerEvent } from '../../event-bus/index';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
-
 /**
  * @description
  * Contains methods relating to {@link Seller} entities.
@@ -24,6 +24,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class SellerService {
     constructor(
         private connection: TransactionalConnection,

+ 3 - 0
packages/core/src/service/services/session.service.ts

@@ -5,6 +5,7 @@ import ms from 'ms';
 import { Brackets, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { Logger } from '../../config';
 import { ConfigService } from '../../config/config.service';
 import { CachedSession, SessionCacheStrategy } from '../../config/session-cache/session-cache-strategy';
@@ -22,6 +23,7 @@ import { RequestContextService } from '../helpers/request-context/request-contex
 import { getUserChannelsPermissions } from '../helpers/utils/get-user-channels-permissions';
 
 import { OrderService } from './order.service';
+
 /**
  * @description
  * Contains methods relating to {@link Session} entities.
@@ -29,6 +31,7 @@ import { OrderService } from './order.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class SessionService implements EntitySubscriberInterface, OnApplicationBootstrap {
     private sessionCacheStrategy: SessionCacheStrategy;
     private cleanSessionsJobQueue: JobQueue<{ batchSize: number }>;

+ 2 - 0
packages/core/src/service/services/shipping-method.service.ts

@@ -16,6 +16,7 @@ import { IsNull } from 'typeorm';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { EntityNotFoundError, ForbiddenError, UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -42,6 +43,7 @@ import { RoleService } from './role.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ShippingMethodService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/stock-level.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { AvailableStock } from '../../config/catalog/stock-location-strategy';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -20,6 +21,7 @@ import { StockLocationService } from './stock-location.service';
  * @since 2.0.0
  */
 @Injectable()
+@Instrument()
 export class StockLevelService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/stock-location.service.ts

@@ -15,6 +15,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
 import { EntityNotFoundError, ForbiddenError, UserInputError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -32,6 +33,7 @@ import { ChannelService } from './channel.service';
 import { RoleService } from './role.service';
 
 @Injectable()
+@Instrument()
 export class StockLocationService {
     constructor(
         private requestContextService: RequestContextService,

+ 3 - 3
packages/core/src/service/services/stock-movement.service.ts

@@ -9,13 +9,13 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { In } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { idsAreEqual } from '../../common/utils';
-import { ConfigService } from '../../config/config.service';
 import { ShippingCalculator } from '../../config/shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Order } from '../../entity/order/order.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
 import { Allocation } from '../../entity/stock-movement/allocation.entity';
@@ -39,6 +39,7 @@ import { StockLocationService } from './stock-location.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class StockMovementService {
     shippingEligibilityCheckers: ShippingEligibilityChecker[];
     shippingCalculators: ShippingCalculator[];
@@ -51,7 +52,6 @@ export class StockMovementService {
         private stockLevelService: StockLevelService,
         private eventBus: EventBus,
         private stockLocationService: StockLocationService,
-        private configService: ConfigService,
     ) {}
 
     /**

+ 6 - 1
packages/core/src/service/services/tag.service.ts

@@ -9,6 +9,7 @@ import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions, Taggable } from '../../common/types/common-types';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { VendureEntity } from '../../entity/base/base.entity';
@@ -22,8 +23,12 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class TagService {
-    constructor(private connection: TransactionalConnection, private listQueryBuilder: ListQueryBuilder) {}
+    constructor(
+        private connection: TransactionalConnection,
+        private listQueryBuilder: ListQueryBuilder,
+    ) {}
 
     findAll(ctx: RequestContext, options?: ListQueryOptions<Tag>): Promise<PaginatedList<Tag>> {
         return this.listQueryBuilder

+ 2 - 0
packages/core/src/service/services/tax-category.service.ts

@@ -9,6 +9,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { EntityNotFoundError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -26,6 +27,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class TaxCategoryService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/tax-rate.service.ts

@@ -10,6 +10,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { EntityNotFoundError } from '../../common/error/errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { createSelfRefreshingCache, SelfRefreshingCache } from '../../common/self-refreshing-cache';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
@@ -33,6 +34,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class TaxRateService {
     private readonly defaultTaxRate = new TaxRate({
         value: 0,

+ 2 - 0
packages/core/src/service/services/user.service.ts

@@ -18,6 +18,7 @@ import {
     VerificationTokenExpiredError,
     VerificationTokenInvalidError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { Instrument } from '../../common/instrument-decorator';
 import { isEmailAddressLike, normalizeEmailAddress } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -35,6 +36,7 @@ import { RoleService } from './role.service';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class UserService {
     constructor(
         private connection: TransactionalConnection,

+ 2 - 0
packages/core/src/service/services/zone.service.ts

@@ -12,6 +12,7 @@ import { unique } from '@vendure/common/lib/unique';
 import { In } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { Instrument } from '../../common/instrument-decorator';
 import { createSelfRefreshingCache, SelfRefreshingCache } from '../../common/self-refreshing-cache';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
@@ -34,6 +35,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  * @docsCategory services
  */
 @Injectable()
+@Instrument()
 export class ZoneService {
     /**
      * We cache all Zones to avoid hitting the DB many times per request.

+ 1 - 1
packages/create/src/helpers.ts

@@ -1,7 +1,7 @@
 import { cancel, isCancel, spinner } from '@clack/prompts';
 import spawn from 'cross-spawn';
 import fs from 'fs-extra';
-import { execFile, execSync, execFileSync } from 'node:child_process';
+import { execFile, execFileSync, execSync } from 'node:child_process';
 import { platform } from 'node:os';
 import { promisify } from 'node:util';
 import path from 'path';

+ 0 - 1
packages/dashboard/src/app/routes/_authenticated/_orders/orders.graphql.ts

@@ -1,6 +1,5 @@
 import { assetFragment, errorResultFragment } from '@/graphql/fragments.js';
 import { graphql } from '@/graphql/graphql.js';
-import { gql } from 'awesome-graphql-client';
 
 export const orderListDocument = graphql(`
     query GetOrders($options: OrderListOptions) {

+ 0 - 1
packages/dashboard/src/lib/framework/defaults.ts

@@ -9,7 +9,6 @@ import {
     Users,
 } from 'lucide-react';
 
-import { registerAlert } from './alert/alert-extensions.js';
 import { LatestOrdersWidget } from './dashboard-widget/latest-orders-widget/index.js';
 import { MetricsWidget } from './dashboard-widget/metrics-widget/index.js';
 import { OrdersSummaryWidget } from './dashboard-widget/orders-summary/index.js';

+ 1 - 2
packages/dashboard/src/lib/framework/document-introspection/get-document-structure.ts

@@ -2,11 +2,10 @@ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { VariablesOf } from 'gql.tada';
 import {
     DocumentNode,
-    OperationDefinitionNode,
     FieldNode,
     FragmentDefinitionNode,
     FragmentSpreadNode,
-    VariableDefinitionNode,
+    OperationDefinitionNode,
 } from 'graphql';
 import { DefinitionNode, NamedTypeNode, SelectionSetNode, TypeNode } from 'graphql/language/ast.js';
 import { schemaInfo } from 'virtual:admin-api-schema';

+ 4 - 2
packages/dashboard/src/lib/framework/registry/registry-types.ts

@@ -1,7 +1,9 @@
 import { DashboardAlertDefinition } from '../alert/types.js';
 import { DashboardWidgetDefinition } from '../dashboard-widget/types.js';
-import { DashboardActionBarItem } from '../extension-api/extension-api-types.js';
-import { DashboardPageBlockDefinition } from '../extension-api/extension-api-types.js';
+import {
+    DashboardActionBarItem,
+    DashboardPageBlockDefinition,
+} from '../extension-api/extension-api-types.js';
 import { NavMenuConfig } from '../nav-menu/nav-menu-extensions.js';
 
 export interface GlobalRegistryContents {

+ 1 - 1
packages/dashboard/src/lib/graphql/graphql-env.d.ts

@@ -504,7 +504,7 @@ export type introspection = {
     types: introspection_types;
 };
 
-import * as gqlTada from 'gql.tada';
+import 'gql.tada';
 
 declare module 'gql.tada' {
     interface setupSchema {

+ 0 - 1
packages/dashboard/src/lib/hooks/use-local-format.ts

@@ -1,4 +1,3 @@
-import { useLingui } from '@lingui/react';
 import { useCallback, useMemo } from 'react';
 
 import { useServerConfig } from './use-server-config.js';

+ 3 - 3
packages/dashboard/src/lib/index.ts

@@ -32,6 +32,7 @@ export * from './components/shared/asset/asset-gallery.js';
 export * from './components/shared/asset/asset-picker-dialog.js';
 export * from './components/shared/asset/asset-preview-dialog.js';
 export * from './components/shared/asset/asset-preview.js';
+export * from './components/shared/asset/focal-point-control.js';
 export * from './components/shared/assigned-facet-values.js';
 export * from './components/shared/channel-code-label.js';
 export * from './components/shared/channel-selector.js';
@@ -50,7 +51,6 @@ export * from './components/shared/entity-assets.js';
 export * from './components/shared/error-page.js';
 export * from './components/shared/facet-value-chip.js';
 export * from './components/shared/facet-value-selector.js';
-export * from './components/shared/asset/focal-point-control.js';
 export * from './components/shared/form-field-wrapper.js';
 export * from './components/shared/history-timeline/history-entry.js';
 export * from './components/shared/history-timeline/history-note-checkbox.js';
@@ -113,8 +113,8 @@ export * from './framework/dashboard-widget/metrics-widget/index.js';
 export * from './framework/dashboard-widget/metrics-widget/metrics-widget.graphql.js';
 export * from './framework/dashboard-widget/orders-summary/index.js';
 export * from './framework/dashboard-widget/orders-summary/order-summary-widget.graphql.js';
-export * from './framework/dashboard-widget/widget-extensions.js';
 export * from './framework/dashboard-widget/types.js';
+export * from './framework/dashboard-widget/widget-extensions.js';
 export * from './framework/defaults.js';
 export * from './framework/document-introspection/add-custom-fields.js';
 export * from './framework/document-introspection/get-document-structure.js';
@@ -124,9 +124,9 @@ export * from './framework/extension-api/extension-api-types.js';
 export * from './framework/extension-api/use-dashboard-extensions.js';
 export * from './framework/form-engine/form-schema-tools.js';
 export * from './framework/form-engine/use-generated-form.js';
+export * from './framework/layout-engine/layout-extensions.js';
 export * from './framework/layout-engine/location-wrapper.js';
 export * from './framework/layout-engine/page-layout.js';
-export * from './framework/layout-engine/layout-extensions.js';
 export * from './framework/nav-menu/nav-menu-extensions.js';
 export * from './framework/page/detail-page-route-loader.js';
 export * from './framework/page/detail-page.js';

+ 2 - 2
packages/dashboard/vite/utils/ast-utils.spec.ts

@@ -1,7 +1,7 @@
 import ts from 'typescript';
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
 
-import { getPluginInfo, findConfigExport } from './ast-utils.js';
+import { findConfigExport, getPluginInfo } from './ast-utils.js';
 
 describe('getPluginInfo', () => {
     it('should return undefined when no plugin class is found', () => {

+ 4 - 5
packages/dashboard/vite/utils/schema-generator.ts

@@ -1,15 +1,14 @@
 import { GraphQLTypesLoader } from '@nestjs/graphql';
 import {
-    resetConfig,
-    setConfig,
     getConfig,
-    runPluginConfigurations,
     getFinalVendureSchema,
+    resetConfig,
+    runPluginConfigurations,
+    setConfig,
     VENDURE_ADMIN_API_TYPE_PATHS,
     VendureConfig,
 } from '@vendure/core';
-import { buildSchema } from 'graphql';
-import { GraphQLSchema } from 'graphql';
+import { buildSchema, GraphQLSchema } from 'graphql';
 
 let schemaPromise: Promise<GraphQLSchema> | undefined;
 

+ 7 - 3
packages/dashboard/vite/utils/ui-config.ts

@@ -1,13 +1,17 @@
 import {
+    ADMIN_API_PATH,
     DEFAULT_AUTH_TOKEN_HEADER_KEY,
     DEFAULT_CHANNEL_TOKEN_KEY,
-    ADMIN_API_PATH,
 } from '@vendure/common/lib/shared-constants';
 import { AdminUiConfig } from '@vendure/common/lib/shared-types';
 import { VendureConfig } from '@vendure/core';
 
-import { defaultAvailableLocales } from '../constants.js';
-import { defaultLocale, defaultLanguage, defaultAvailableLanguages } from '../constants.js';
+import {
+    defaultAvailableLanguages,
+    defaultAvailableLocales,
+    defaultLanguage,
+    defaultLocale,
+} from '../constants.js';
 
 export function getAdminUiConfig(
     config: VendureConfig,

+ 0 - 10
packages/dashboard/vite/vite-plugin-admin-api-schema.ts

@@ -1,14 +1,4 @@
-import { GraphQLTypesLoader } from '@nestjs/graphql';
 import {
-    getConfig,
-    getFinalVendureSchema,
-    resetConfig,
-    runPluginConfigurations,
-    setConfig,
-    VENDURE_ADMIN_API_TYPE_PATHS,
-} from '@vendure/core';
-import {
-    buildSchema,
     GraphQLList,
     GraphQLNonNull,
     GraphQLObjectType,

+ 1 - 19
packages/dev-server/dev-config.ts

@@ -15,6 +15,7 @@ import {
 } from '@vendure/core';
 import { ScheduledTask } from '@vendure/core/dist/scheduler/scheduled-task';
 import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
+import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
 import 'dotenv/config';
 import { GraphiqlPlugin } from '@vendure/graphiql-plugin';
 import path from 'path';
@@ -94,25 +95,6 @@ export const devConfig: VendureConfig = {
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
-    schedulerOptions: {
-        tasks: [
-            new ScheduledTask({
-                id: 'test-job',
-                description: "A test job that doesn't do anything",
-                schedule: '*/20 * * * * *',
-                async execute(injector) {
-                    await new Promise(resolve => setTimeout(resolve, 10_000));
-                    return { success: true };
-                },
-            }),
-            // cleanSessionsTask.configure({
-            //     schedule: cron => cron.every(1).minutes(),
-            //     params: {
-            //         batchSize: 10,
-            //     },
-            // }),
-        ],
-    },
     plugins: [
         // MultivendorPlugin.init({
         //     platformFeePercent: 10,

+ 26 - 0
packages/dev-server/instrumentation.ts

@@ -0,0 +1,26 @@
+import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
+import { NodeSDK } from '@opentelemetry/sdk-node';
+import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
+import { getSdkConfiguration } from '@vendure/telemetry-plugin/preload';
+
+process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:3100/otlp';
+process.env.OTEL_LOGS_EXPORTER = 'otlp';
+process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.name=vendure-dev-server';
+
+const traceExporter = new OTLPTraceExporter({
+    url: 'http://localhost:4318/v1/traces',
+});
+const logExporter = new OTLPLogExporter();
+
+const config = getSdkConfiguration({
+    config: {
+        spanProcessors: [new BatchSpanProcessor(traceExporter)],
+        logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
+    },
+});
+
+const sdk = new NodeSDK(config);
+
+sdk.start();

+ 111 - 0
packages/graphiql-plugin/e2e/graphiql-plugin.e2e-spec.ts

@@ -0,0 +1,111 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { ConfigService, LanguageCode, mergeConfig } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { GraphiQLService } from '../src/graphiql.service';
+import { GraphiqlPlugin } from '../src/plugin';
+
+describe('GraphiQLPlugin', () => {
+    let serviceInstance: GraphiQLService;
+    let configService: ConfigService;
+
+    const { server, adminClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            apiOptions: {
+                adminApiPlayground: true,
+                shopApiPlayground: true,
+            },
+            plugins: [GraphiqlPlugin.init()],
+        }),
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            initialData: {
+                defaultLanguage: LanguageCode.en,
+                defaultZone: 'Europe/London',
+                countries: [],
+                taxRates: [],
+                paymentMethods: [],
+                shippingMethods: [],
+                collections: [],
+            },
+        });
+        await adminClient.asSuperAdmin();
+        configService = server.app.get(ConfigService);
+        serviceInstance = server.app.get(GraphiQLService);
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('configuration', () => {
+        it('should disable GraphQL playground in config', async () => {
+            expect(configService.apiOptions.adminApiPlayground).toBe(false);
+            expect(configService.apiOptions.shopApiPlayground).toBe(false);
+        });
+    });
+
+    describe('GraphiQLService', () => {
+        describe('getAdminApiUrl', () => {
+            it('should return the admin API URL', () => {
+                configService.apiOptions.adminApiPath = 'admin-api';
+                const url = serviceInstance.getAdminApiUrl();
+                expect(url).toBe('/admin-api');
+            });
+
+            it('should use default path if not specified', () => {
+                configService.apiOptions.adminApiPath = 'admin-api';
+                const url = serviceInstance.getAdminApiUrl();
+                expect(url).toBe('/admin-api');
+            });
+        });
+
+        describe('getShopApiUrl', () => {
+            it('should return the shop API URL', () => {
+                configService.apiOptions.shopApiPath = 'shop-api';
+                const url = serviceInstance.getShopApiUrl();
+                expect(url).toBe('/shop-api');
+            });
+
+            it('should use default path if not specified', () => {
+                configService.apiOptions.shopApiPath = 'shop-api';
+                const url = serviceInstance.getShopApiUrl();
+                expect(url).toBe('/shop-api');
+            });
+        });
+
+        describe('createApiUrl', () => {
+            it('should create a relative URL if no host is specified', () => {
+                configService.apiOptions.hostname = '';
+                configService.apiOptions.port = 3000;
+                const url = (serviceInstance as any).createApiUrl('test-api');
+                expect(url).toBe('/test-api');
+            });
+
+            it('should create an absolute URL if host is specified', () => {
+                configService.apiOptions.hostname = 'example.com';
+                configService.apiOptions.port = 3000;
+                const url = (serviceInstance as any).createApiUrl('test-api');
+                expect(url).toBe('http://example.com:3000/test-api');
+            });
+
+            it('should handle HTTPS hosts', () => {
+                configService.apiOptions.hostname = 'https://example.com';
+                configService.apiOptions.port = 443;
+                const url = (serviceInstance as any).createApiUrl('test-api');
+                expect(url).toBe('https://example.com:443/test-api');
+            });
+
+            it('should handle paths with leading slash', () => {
+                configService.apiOptions.hostname = 'example.com';
+                configService.apiOptions.port = 3000;
+                const url = (serviceInstance as any).createApiUrl('/test-api');
+                expect(url).toBe('http://example.com:3000/test-api');
+            });
+        });
+    });
+});

+ 4 - 3
packages/graphiql-plugin/package.json

@@ -17,7 +17,8 @@
         "build:app": "vite build",
         "dev": "vite",
         "lint": "eslint --fix .",
-        "test": "vitest --config vitest.config.mts --run"
+        "test": "vitest --config vitest.config.mts --run",
+        "e2e": "cross-env PACKAGE=graphiql-plugin vitest --config ../../e2e-common/vitest.config.mts --run"
     },
     "homepage": "https://www.vendure.io/",
     "funding": "https://github.com/sponsors/michaelbromley",
@@ -32,8 +33,8 @@
         "@types/express": "^5.0.0",
         "@types/react": "^19.0.0",
         "@types/react-dom": "^19.0.0",
-        "@vendure/common": "3.2.3",
-        "@vendure/core": "3.2.3",
+        "@vendure/common": "3.2.4",
+        "@vendure/core": "3.2.4",
         "@vitejs/plugin-react": "^4.3.4",
         "graphiql": "^3.8.3",
         "react": "^19.0.0",

+ 0 - 1
packages/graphiql-plugin/src/index.ts

@@ -3,6 +3,5 @@
  * The GraphiQL plugin exports a server plugin which serves
  * a GraphQL playground for the admin & shop APIs.
  */
-
 export * from './plugin';
 export * from './types';

+ 3 - 148
packages/graphiql-plugin/src/plugin.spec.ts

@@ -1,29 +1,17 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import { Test, TestingModule } from '@nestjs/testing';
 import { TypeOrmModule } from '@nestjs/typeorm';
-import { ConfigService, LanguageCode, Logger, PluginCommonModule, ProcessContext } from '@vendure/core';
-import {
-    createTestEnvironment,
-    registerInitializer,
-    SqljsInitializer,
-    testConfig,
-    TestEnvironment,
-    TestingLogger,
-} from '@vendure/testing';
+import { Logger, PluginCommonModule, ProcessContext } from '@vendure/core';
+import { TestingLogger } from '@vendure/testing';
 import express from 'express';
 import fs from 'fs';
-import path from 'path';
-import { afterEach, beforeEach, beforeAll, afterAll, describe, expect, it, Mock, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
 
 import { PLUGIN_INIT_OPTIONS } from './constants';
 import { GraphiQLService } from './graphiql.service';
 import { GraphiqlPlugin } from './plugin';
 import { GraphiqlPluginOptions } from './types';
 
-const sqliteDataDir = path.join(__dirname, '__data__');
-
-registerInitializer('sqljs', new SqljsInitializer(sqliteDataDir));
-
 // Use this type instead of NestJS's MiddlewareConsumer which is not exported
 interface MockMiddlewareConsumer {
     apply: Mock;
@@ -90,45 +78,6 @@ describe('GraphiQLPlugin', () => {
         });
     });
 
-    describe(
-        'configuration',
-        () => {
-            it('should disable GraphQL playground in config', async () => {
-                const result = createTestEnvironment({
-                    ...testConfig,
-                    apiOptions: {
-                        ...testConfig.apiOptions,
-                        adminApiPlayground: true,
-                        shopApiPlayground: true,
-                    },
-                    plugins: [GraphiqlPlugin.init()],
-                });
-
-                await result.server.init({
-                    initialData: {
-                        defaultLanguage: LanguageCode.en,
-                        defaultZone: 'Europe/London',
-                        countries: [],
-                        taxRates: [],
-                        paymentMethods: [],
-                        shippingMethods: [],
-                        collections: [],
-                    },
-                });
-
-                const configService = result.server.app.get(ConfigService);
-
-                expect(configService.apiOptions.adminApiPlayground).toBe(false);
-                expect(configService.apiOptions.shopApiPlayground).toBe(false);
-
-                await result.server.destroy();
-            });
-        },
-        {
-            timeout: 60000,
-        },
-    );
-
     describe('configure middleware', () => {
         it('should not configure middleware if not running in server', async () => {
             const plugin = await createPlugin(undefined, false);
@@ -376,100 +325,6 @@ describe('GraphiQLPlugin', () => {
     });
 });
 
-describe('GraphiQLService', () => {
-    let serviceInstance: GraphiQLService;
-    let configService: ConfigService;
-    let result: TestEnvironment;
-    beforeAll(async () => {
-        result = createTestEnvironment({
-            ...testConfig,
-            apiOptions: {
-                ...testConfig.apiOptions,
-                adminApiPlayground: true,
-                shopApiPlayground: true,
-            },
-            plugins: [GraphiqlPlugin.init()],
-        });
-
-        await result.server.init({
-            initialData: {
-                defaultLanguage: LanguageCode.en,
-                defaultZone: 'Europe/London',
-                countries: [],
-                taxRates: [],
-                paymentMethods: [],
-                shippingMethods: [],
-                collections: [],
-            },
-        });
-
-        configService = result.server.app.get(ConfigService);
-        serviceInstance = result.server.app.get(GraphiQLService);
-    });
-
-    afterAll(async () => {
-        await result.server.destroy();
-    });
-
-    describe('getAdminApiUrl', () => {
-        it('should return the admin API URL', () => {
-            configService.apiOptions.adminApiPath = 'admin-api';
-            const url = serviceInstance.getAdminApiUrl();
-            expect(url).toBe('/admin-api');
-        });
-
-        it('should use default path if not specified', () => {
-            configService.apiOptions.adminApiPath = 'admin-api';
-            const url = serviceInstance.getAdminApiUrl();
-            expect(url).toBe('/admin-api');
-        });
-    });
-
-    describe('getShopApiUrl', () => {
-        it('should return the shop API URL', () => {
-            configService.apiOptions.shopApiPath = 'shop-api';
-            const url = serviceInstance.getShopApiUrl();
-            expect(url).toBe('/shop-api');
-        });
-
-        it('should use default path if not specified', () => {
-            configService.apiOptions.shopApiPath = 'shop-api';
-            const url = serviceInstance.getShopApiUrl();
-            expect(url).toBe('/shop-api');
-        });
-    });
-
-    describe('createApiUrl', () => {
-        it('should create a relative URL if no host is specified', () => {
-            configService.apiOptions.hostname = '';
-            configService.apiOptions.port = 3000;
-            const url = (serviceInstance as any).createApiUrl('test-api');
-            expect(url).toBe('/test-api');
-        });
-
-        it('should create an absolute URL if host is specified', () => {
-            configService.apiOptions.hostname = 'example.com';
-            configService.apiOptions.port = 3000;
-            const url = (serviceInstance as any).createApiUrl('test-api');
-            expect(url).toBe('http://example.com:3000/test-api');
-        });
-
-        it('should handle HTTPS hosts', () => {
-            configService.apiOptions.hostname = 'https://example.com';
-            configService.apiOptions.port = 443;
-            const url = (serviceInstance as any).createApiUrl('test-api');
-            expect(url).toBe('https://example.com:443/test-api');
-        });
-
-        it('should handle paths with leading slash', () => {
-            configService.apiOptions.hostname = 'example.com';
-            configService.apiOptions.port = 3000;
-            const url = (serviceInstance as any).createApiUrl('/test-api');
-            expect(url).toBe('http://example.com:3000/test-api');
-        });
-    });
-});
-
 // Helper functions
 function createMockConsumer(): MockMiddlewareConsumer {
     return {

+ 2 - 2
packages/graphiql-plugin/src/plugin.ts

@@ -9,7 +9,6 @@ import {
     VendurePlugin,
 } from '@vendure/core';
 import express from 'express';
-import rateLimit from 'express-rate-limit';
 import fs from 'fs';
 import path from 'path';
 
@@ -56,6 +55,7 @@ import { GraphiqlPluginOptions } from './types';
         config.apiOptions.shopApiPlayground = false;
         return config;
     },
+    exports: [GraphiQLService],
     compatibility: '^3.0.0',
 })
 export class GraphiqlPlugin implements NestModule {
@@ -75,7 +75,7 @@ export class GraphiqlPlugin implements NestModule {
             ...options,
             route: options.route || 'graphiql',
         };
-        return this;
+        return GraphiqlPlugin;
     }
 
     configure(consumer: MiddlewareConsumer) {

+ 52 - 0
packages/telemetry-plugin/package.json

@@ -0,0 +1,52 @@
+{
+    "name": "@vendure/telemetry-plugin",
+    "version": "3.2.2",
+    "description": "Telemetry for Vendure",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/vendure-ecommerce/vendure/"
+    },
+    "homepage": "https://www.vendure.io",
+    "funding": "https://github.com/sponsors/michaelbromley",
+    "private": false,
+    "license": "GPL-3.0-or-later",
+    "type": "commonjs",
+    "scripts": {
+        "build": "rimraf dist && tsc -p ./tsconfig.build.json",
+        "watch": "tsc -p ./tsconfig.build.json --watch"
+    },
+    "publishConfig": {
+        "access": "public"
+    },
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "files": [
+        "dist/**/*"
+    ],
+    "exports": {
+        ".": {
+            "types": "./dist/index.d.ts",
+            "default": "./dist/index.js"
+        },
+        "./preload": {
+            "types": "./dist/instrumentation.d.ts",
+            "default": "./dist/instrumentation.js"
+        }
+    },
+    "dependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/auto-instrumentations-node": "^0.58.0",
+        "@opentelemetry/context-async-hooks": "^2.0.0",
+        "@opentelemetry/exporter-logs-otlp-proto": "^0.200.0",
+        "@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
+        "@opentelemetry/resources": "^2.0.0",
+        "@opentelemetry/sdk-logs": "^0.200.0",
+        "@opentelemetry/sdk-node": "^0.200.0",
+        "javascript-stringify": "^2.1.0"
+    },
+    "devDependencies": {
+        "@vendure/common": "3.2.4",
+        "@vendure/core": "3.2.4",
+        "typescript": "5.8.2"
+    }
+}

+ 74 - 0
packages/telemetry-plugin/src/config/default-method-hooks.ts

@@ -0,0 +1,74 @@
+import { CacheService, EventBus, JobQueue, JobQueueService } from '@vendure/core';
+import { stringify } from 'javascript-stringify';
+
+import { registerMethodHooks } from '../service/method-hooks.service';
+
+export const defaultMethodHooks = [
+    registerMethodHooks(CacheService, {
+        get: {
+            pre: ({ args: [key], span }) => {
+                span.setAttribute('cache.key', key);
+            },
+            post: ({ args: [key], result: hit, span }) => {
+                span.setAttribute('cache.hit', !!hit);
+                if (hit) {
+                    span.addEvent('cache.hit', { key });
+                } else {
+                    span.addEvent('cache.miss', { key });
+                }
+            },
+        },
+        set: {
+            pre: ({ args: [key], span }) => {
+                span.setAttribute('cache.key', key);
+            },
+        },
+        delete: {
+            pre: ({ args: [key], span }) => {
+                span.setAttribute('cache.key', key);
+            },
+        },
+        invalidateTags: {
+            pre: ({ args: [tags], span }) => {
+                span.setAttribute('cache.tags', tags.join(', '));
+            },
+        },
+    }),
+    registerMethodHooks(EventBus, {
+        publish: {
+            pre: ({ args: [event], span }) => {
+                span.setAttribute('event', event.constructor.name);
+                span.setAttribute('event.timestamp', event.createdAt.toISOString());
+            },
+        },
+    }),
+    registerMethodHooks(JobQueueService, {
+        createQueue: {
+            pre: ({ args: [options], span }) => {
+                span.setAttribute('job-queue.name', options.name);
+            },
+        },
+    }),
+    registerMethodHooks(JobQueue, {
+        start: {
+            pre: ({ instance, span }) => {
+                span.setAttribute('job-queue.name', instance.name);
+            },
+        },
+        add: {
+            pre: ({ args: [data, options], span, instance }) => {
+                span.setAttribute('job.queueName', instance.name);
+                span.setAttribute(
+                    'job.data',
+                    stringify(data, null, 2, {
+                        maxDepth: 3,
+                    }) ?? '',
+                );
+                span.setAttribute('job.retries', options?.retries ?? 0);
+            },
+            post({ result, span }) {
+                span.setAttribute('job.buffered', result.id === 'buffered');
+            },
+        },
+    }),
+];

+ 63 - 0
packages/telemetry-plugin/src/config/otel-instrumentation-strategy.ts

@@ -0,0 +1,63 @@
+import { Span as ApiSpan, SpanStatusCode, trace } from '@opentelemetry/api';
+import { Injector, InstrumentationStrategy, VENDURE_VERSION, WrappedMethodArgs } from '@vendure/core';
+
+import { MethodHooksService } from '../service/method-hooks.service';
+
+export const tracer = trace.getTracer('@vendure/core', VENDURE_VERSION);
+
+const recordException = (span: ApiSpan, error: any) => {
+    span.recordException(error);
+    span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
+};
+
+export class OtelInstrumentationStrategy implements InstrumentationStrategy {
+    private spanAttributeService: MethodHooksService;
+
+    init(injector: Injector) {
+        this.spanAttributeService = injector.get(MethodHooksService);
+    }
+
+    wrapMethod({ instance, target, methodName, args, applyOriginalFunction }: WrappedMethodArgs) {
+        const spanName = `${String(target.name)}.${String(methodName)}`;
+
+        return tracer.startActiveSpan(spanName, {}, span => {
+            const hooks = this.spanAttributeService?.getHooks(target, methodName);
+            hooks?.pre?.({ args, span, instance });
+            if (applyOriginalFunction.constructor.name === 'AsyncFunction') {
+                return (
+                    applyOriginalFunction()
+                        .then((result: any) => {
+                            if (hooks?.post) {
+                                hooks.post({ args, result, span, instance });
+                            }
+                            return result;
+                        })
+                        // @ts-expect-error
+                        .catch(error => {
+                            recordException(span, error);
+                            // Throw error to propagate it further
+                            throw error;
+                        })
+                        .finally(() => {
+                            span.end();
+                        })
+                );
+            }
+
+            try {
+                const result = applyOriginalFunction();
+                if (hooks?.post) {
+                    hooks.post({ args, result, span, instance });
+                }
+                return result;
+            } catch (error) {
+                recordException(span, error);
+
+                // throw for further propagation
+                throw error;
+            } finally {
+                span.end();
+            }
+        });
+    }
+}

+ 58 - 0
packages/telemetry-plugin/src/config/otel-logger.ts

@@ -0,0 +1,58 @@
+import { logs, SeverityNumber } from '@opentelemetry/api-logs';
+import { DefaultLogger, LogLevel, VENDURE_VERSION, VendureLogger } from '@vendure/core';
+
+export const otelLogger = logs.getLogger('@vendure/core', VENDURE_VERSION);
+
+export interface OtelLoggerOptions {
+    logToConsole?: LogLevel;
+}
+
+export class OtelLogger implements VendureLogger {
+    private defaultLogger?: DefaultLogger;
+
+    constructor(options: OtelLoggerOptions) {
+        if (options.logToConsole) {
+            this.defaultLogger = new DefaultLogger({
+                level: options.logToConsole,
+                timestamp: false,
+            });
+        }
+    }
+
+    debug(message: string, context?: string): void {
+        this.emitLog(SeverityNumber.DEBUG, message, context);
+        this.defaultLogger?.debug(message, context);
+    }
+
+    warn(message: string, context?: string): void {
+        this.emitLog(SeverityNumber.WARN, message, context);
+        this.defaultLogger?.warn(message, context);
+    }
+
+    info(message: string, context?: string): void {
+        this.emitLog(SeverityNumber.INFO, message, context);
+        this.defaultLogger?.info(message, context);
+    }
+
+    error(message: string, context?: string): void {
+        this.emitLog(SeverityNumber.ERROR, message, context);
+        this.defaultLogger?.error(message, context);
+    }
+
+    verbose(message: string, context?: string): void {
+        this.emitLog(SeverityNumber.DEBUG, message, context);
+        this.defaultLogger?.verbose(message, context);
+    }
+
+    private emitLog(severityNumber: SeverityNumber, message: string, context?: string, label?: string): void {
+        otelLogger.emit({
+            severityNumber,
+            body: message,
+            attributes: {
+                context,
+                service_name: 'vendure',
+                ...(label ? { label } : {}),
+            },
+        });
+    }
+}

+ 1 - 0
packages/telemetry-plugin/src/constants.ts

@@ -0,0 +1 @@
+export const TELEMETRY_PLUGIN_OPTIONS = Symbol('TELEMETRY_PLUGIN_OPTIONS');

+ 6 - 0
packages/telemetry-plugin/src/index.ts

@@ -0,0 +1,6 @@
+export * from './config/default-method-hooks';
+export * from './config/otel-instrumentation-strategy';
+export * from './config/otel-logger';
+export * from './service/method-hooks.service';
+export * from './telemetry.plugin';
+export * from './types';

+ 116 - 0
packages/telemetry-plugin/src/instrumentation.ts

@@ -0,0 +1,116 @@
+import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
+import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
+import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { resourceFromAttributes } from '@opentelemetry/resources';
+import {
+    BatchLogRecordProcessor,
+    ConsoleLogRecordExporter,
+    SimpleLogRecordProcessor,
+} from '@opentelemetry/sdk-logs';
+import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
+import { BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
+// Deep import is intentional: otherwise unwanted code (such as instrumented classes) will get
+// loaded too early before the Otel instrumentation has had a chance to do its thing.
+import { ENABLE_INSTRUMENTATION_ENV_VAR } from '@vendure/core/dist/common/instrument-decorator';
+
+const traceExporter = new OTLPTraceExporter();
+const logExporter = new OTLPLogExporter();
+
+/**
+ * @description
+ * Options for configuring the OpenTelemetry Node SDK.
+ *
+ * @docsCategory core plugins/TelemetryPlugin
+ * @docsPage getSdkConfiguration
+ */
+export interface SdkConfigurationOptions {
+    /**
+     * @description
+     * When set to `true`, the SDK will log spans to the console instead of sending them to an
+     * exporter. This should just be used for debugging purposes.
+     *
+     * @default false
+     */
+    logToConsole?: boolean;
+    /**
+     * @description
+     * The configuration object for the OpenTelemetry Node SDK.
+     */
+    config: Partial<NodeSDKConfiguration>;
+}
+
+/**
+ * @description
+ * Creates a configuration object for the OpenTelemetry Node SDK. This is used to set up a custom
+ * preload script which must be run before the main Vendure server is loaded by means of the
+ * Node.js `--require` flag.
+ *
+ * @example
+ * ```ts
+ * // instrumentation.ts
+ * import { OTLPLogExporter } from '\@opentelemetry/exporter-logs-otlp-proto';
+ * import { OTLPTraceExporter } from '\@opentelemetry/exporter-trace-otlp-http';
+ * import { BatchLogRecordProcessor } from '\@opentelemetry/sdk-logs';
+ * import { NodeSDK } from '\@opentelemetry/sdk-node';
+ * import { BatchSpanProcessor } from '\@opentelemetry/sdk-trace-base';
+ * import { getSdkConfiguration } from '\@vendure/telemetry-plugin/preload';
+ *
+ * process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:3100/otlp';
+ * process.env.OTEL_LOGS_EXPORTER = 'otlp';
+ * process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.name=vendure-dev-server';
+ *
+ * const traceExporter = new OTLPTraceExporter({
+ *     url: 'http://localhost:4318/v1/traces',
+ * });
+ * const logExporter = new OTLPLogExporter();
+ *
+ * const config = getSdkConfiguration({
+ *     config: {
+ *         spanProcessors: [new BatchSpanProcessor(traceExporter)],
+ *         logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
+ *     },
+ * });
+ *
+ * const sdk = new NodeSDK(config);
+ *
+ * sdk.start();
+ * ```
+ *
+ * This would them be run as:
+ * ```bash
+ * node --require ./dist/instrumentation.js ./dist/server.js
+ * ```
+ *
+ * @docsCategory core plugins/TelemetryPlugin
+ * @docsPage getSdkConfiguration
+ * @docsWeight 0
+ */
+export function getSdkConfiguration(options?: SdkConfigurationOptions): Partial<NodeSDKConfiguration> {
+    // This environment variable is used to enable instrumentation in the Vendure core code.
+    // Without setting this env var, no instrumentation will be applied to any Vendure classes.
+    process.env[ENABLE_INSTRUMENTATION_ENV_VAR] = 'true';
+    const { spanProcessors, logRecordProcessors, ...rest } = options?.config ?? {};
+
+    const devModeAwareConfig: Partial<NodeSDKConfiguration> = options?.logToConsole
+        ? {
+              spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
+              logRecordProcessors: [new SimpleLogRecordProcessor(new ConsoleLogRecordExporter())],
+          }
+        : {
+              spanProcessors: spanProcessors ?? [new BatchSpanProcessor(traceExporter)],
+              logRecordProcessors: logRecordProcessors ?? [new BatchLogRecordProcessor(logExporter)],
+          };
+
+    return {
+        resource: resourceFromAttributes({
+            'service.name': 'vendure',
+            'service.namespace': 'vendure',
+            'service.environment': process.env.NODE_ENV || 'development',
+        }),
+        ...devModeAwareConfig,
+        contextManager: new AsyncLocalStorageContextManager(),
+        instrumentations: [getNodeAutoInstrumentations()],
+        ...rest,
+    };
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است