@friggframework/core 2.0.0-next.11 → 2.0.0-next.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +702 -0
- package/README.md +999 -50
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-script-execution-repository-interface.js +171 -0
- package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +238 -0
- package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +278 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/README.md +451 -0
- package/application/commands/admin-script-commands.js +359 -0
- package/application/commands/credential-commands.js +262 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +356 -0
- package/application/commands/integration-mapping-commands.js +25 -0
- package/application/commands/process-commands.js +135 -0
- package/application/commands/report-commands.js +188 -0
- package/application/commands/scheduler-commands.js +263 -0
- package/application/commands/usage-commands.js +56 -0
- package/application/commands/user-commands.js +312 -0
- package/application/index.js +83 -0
- package/artifacts/repositories/artifact-repository-factory.js +19 -0
- package/artifacts/repositories/artifact-repository-interface.js +27 -0
- package/artifacts/repositories/artifact-repository-local.js +42 -0
- package/artifacts/repositories/artifact-repository-s3.js +61 -0
- package/assertions/index.js +0 -3
- package/core/CLAUDE.md +700 -0
- package/core/Worker.js +60 -24
- package/core/create-handler.js +189 -5
- package/core/parameters-to-env.js +257 -0
- package/core/ssm-preload.mjs +34 -0
- package/credential/repositories/credential-active-type.js +32 -0
- package/credential/repositories/credential-repository-documentdb.js +355 -0
- package/credential/repositories/credential-repository-factory.js +54 -0
- package/credential/repositories/credential-repository-interface.js +113 -0
- package/credential/repositories/credential-repository-mongo.js +294 -0
- package/credential/repositories/credential-repository-postgres.js +312 -0
- package/credential/repositories/credential-repository.js +300 -0
- package/credential/use-cases/get-credential-for-user.js +25 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +154 -0
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +192 -0
- package/database/encryption/README.md +839 -0
- package/database/encryption/documentdb-encryption-service.md +3575 -0
- package/database/encryption/encryption-schema-registry.js +401 -0
- package/database/encryption/field-encryption-service.js +254 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +230 -0
- package/database/index.js +21 -21
- package/database/prisma.js +182 -0
- package/database/repositories/health-check-repository-documentdb.js +138 -0
- package/database/repositories/health-check-repository-factory.js +48 -0
- package/database/repositories/health-check-repository-interface.js +82 -0
- package/database/repositories/health-check-repository-mongodb.js +89 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-health-use-case.js +29 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +83 -0
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
- package/database/use-cases/run-database-migration-use-case.js +139 -0
- package/database/use-cases/test-encryption-use-case.js +253 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +94 -0
- package/database/utils/mongodb-schema-init.js +108 -0
- package/database/utils/prisma-runner.js +491 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/encrypt/Cryptor.js +34 -168
- package/encrypt/index.js +1 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +15 -7
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +5 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +5 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +386 -0
- package/generated/prisma-mongodb/index-browser.js +368 -0
- package/generated/prisma-mongodb/index.d.ts +27065 -0
- package/generated/prisma-mongodb/index.js +411 -0
- package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
- package/generated/prisma-mongodb/runtime/edge.js +35 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
- package/generated/prisma-mongodb/runtime/library.js +147 -0
- package/generated/prisma-mongodb/runtime/react-native.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-mongodb/schema.prisma +455 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +393 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +5 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +5 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +407 -0
- package/generated/prisma-postgresql/index-browser.js +389 -0
- package/generated/prisma-postgresql/index.d.ts +29904 -0
- package/generated/prisma-postgresql/index.js +432 -0
- package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
- package/generated/prisma-postgresql/runtime/edge.js +35 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
- package/generated/prisma-postgresql/runtime/library.js +147 -0
- package/generated/prisma-postgresql/runtime/react-native.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-postgresql/schema.prisma +436 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +414 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +67 -0
- package/handlers/app-handler-helpers.js +3 -3
- package/handlers/backend-utils.js +271 -42
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +68 -0
- package/handlers/middleware/admin-auth.js +73 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +4 -15
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +329 -0
- package/handlers/routers/health.js +518 -0
- package/handlers/routers/integration-defined-routers.js +85 -10
- package/handlers/routers/integration-webhook-routers.js +80 -0
- package/handlers/routers/user.js +27 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +427 -0
- package/handlers/workers/dlq-processor.js +63 -0
- package/handlers/workers/integration-defined-workers.js +13 -7
- package/index.js +102 -46
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
- package/infrastructure/scheduler/index.js +33 -0
- package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
- package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
- package/integrations/EXTENSIONS.md +240 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/extension.js +254 -0
- package/integrations/index.js +20 -10
- package/integrations/integration-base.js +730 -74
- package/integrations/integration-router.js +398 -180
- package/integrations/options.js +1 -1
- package/integrations/repositories/config-patch-shared.js +43 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +303 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +120 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +183 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +255 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-documentdb.js +322 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +189 -0
- package/integrations/repositories/integration-repository-mongo.js +450 -0
- package/integrations/repositories/integration-repository-postgres.js +492 -0
- package/integrations/repositories/process-repository-documentdb.js +311 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +136 -0
- package/integrations/repositories/process-repository-mongo.js +262 -0
- package/integrations/repositories/process-repository-postgres.js +380 -0
- package/integrations/repositories/process-update-ops-shared.js +112 -0
- package/integrations/repositories/report-id.js +13 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +113 -0
- package/integrations/tests/doubles/test-integration-repository.js +146 -0
- package/integrations/use-cases/create-integration.js +215 -0
- package/integrations/use-cases/create-process.js +130 -0
- package/integrations/use-cases/delete-integration-for-user.js +119 -0
- package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +88 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +89 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/patch-integration-config.js +39 -0
- package/integrations/use-cases/process-errors.js +28 -0
- package/integrations/use-cases/update-integration-config.js +32 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/use-cases/update-process-metrics.js +222 -0
- package/integrations/use-cases/update-process-state.js +163 -0
- package/integrations/utils/map-integration-dto.js +37 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/logs/logger.js +0 -4
- package/{module-plugin → modules}/index.js +0 -10
- package/modules/module-factory.js +56 -0
- package/modules/module.js +307 -0
- package/modules/repositories/module-repository-documentdb.js +350 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +145 -0
- package/modules/repositories/module-repository-mongo.js +436 -0
- package/modules/repositories/module-repository-postgres.js +481 -0
- package/modules/repositories/module-repository.js +369 -0
- package/modules/requester/api-key.js +52 -0
- package/modules/requester/oauth-2.js +555 -0
- package/modules/requester/requester.js +542 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +71 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +34 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +243 -0
- package/modules/use-cases/refresh-entity-options.js +72 -0
- package/modules/use-cases/test-module-auth.js +72 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +92 -50
- package/prisma-mongodb/schema.prisma +455 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
- package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
- package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
- package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
- package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
- package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
- package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +436 -0
- package/queues/queuer-util.js +103 -21
- package/reporting/README.md +154 -0
- package/reporting/builtin-reports.js +6 -0
- package/reporting/index.js +13 -0
- package/reporting/report-base.js +49 -0
- package/reporting/reports/integrations-report.js +221 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +43 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/telemetry/README.md +331 -0
- package/telemetry/bind-telemetry-context.js +73 -0
- package/telemetry/canonical-counters.js +52 -0
- package/telemetry/exporters.js +85 -0
- package/telemetry/index.js +26 -0
- package/telemetry/instrument-handler.js +87 -0
- package/telemetry/no-op-telemetry.js +67 -0
- package/telemetry/north-star.js +103 -0
- package/telemetry/otel-telemetry.js +213 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +120 -0
- package/telemetry/telemetry-context.js +40 -0
- package/telemetry/telemetry-event-bus.js +58 -0
- package/telemetry/telemetry-runtime.js +147 -0
- package/telemetry/telemetry-service.js +51 -0
- package/telemetry/usage-rollup-subscriber.js +116 -0
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +40 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +219 -0
- package/token/repositories/token-repository-postgres.js +264 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/associations/index.d.ts +0 -17
- package/types/core/index.d.ts +12 -4
- package/types/database/index.d.ts +10 -2
- package/types/encrypt/index.d.ts +5 -3
- package/types/integrations/index.d.ts +3 -8
- package/types/module-plugin/index.d.ts +20 -69
- package/types/syncs/index.d.ts +0 -17
- package/usage/README.md +54 -0
- package/usage/index.js +17 -0
- package/usage/repositories/usage-repository-documentdb.js +194 -0
- package/usage/repositories/usage-repository-factory.js +25 -0
- package/usage/repositories/usage-repository-interface.js +37 -0
- package/usage/repositories/usage-repository-prisma.js +146 -0
- package/usage/tracked-metrics.js +38 -0
- package/usage/usage-windows.js +24 -0
- package/user/repositories/user-repository-documentdb.js +458 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +214 -0
- package/user/repositories/user-repository-mongo.js +323 -0
- package/user/repositories/user-repository-postgres.js +377 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +125 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
- package/websocket/repositories/websocket-connection-repository.js +161 -0
- package/assertions/is-equal.js +0 -17
- package/associations/model.js +0 -54
- package/database/models/IndividualUser.js +0 -76
- package/database/models/OrganizationUser.js +0 -29
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/models/UserModel.js +0 -7
- package/database/models/WebsocketConnection.js +0 -49
- package/database/mongo.js +0 -45
- package/database/mongoose.js +0 -5
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/encrypt/test-encrypt.js +0 -107
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/entity.js +0 -46
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/api-key.js +0 -36
- package/module-plugin/requester/oauth-2.js +0 -219
- package/module-plugin/requester/requester.js +0 -165
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- package/syncs/model.js +0 -62
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { SyncRepositoryInterface } = require('./sync-repository-interface');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PostgreSQL Sync Repository Adapter
|
|
6
|
+
* Handles sync persistence using Prisma with PostgreSQL
|
|
7
|
+
*
|
|
8
|
+
* PostgreSQL-specific characteristics:
|
|
9
|
+
* - Uses implicit join tables for entity relations (_EntityToSync)
|
|
10
|
+
* - Uses Int IDs with autoincrement
|
|
11
|
+
* - Requires ID conversion: String (app layer) ↔ Int (database)
|
|
12
|
+
* - All returned IDs are converted to strings for application layer consistency
|
|
13
|
+
* - Uses connect/disconnect syntax for relations
|
|
14
|
+
*/
|
|
15
|
+
class SyncRepositoryPostgres extends SyncRepositoryInterface {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.prisma = prisma;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert string ID to integer for PostgreSQL queries
|
|
23
|
+
* @private
|
|
24
|
+
* @param {string|number|null|undefined} id - ID to convert
|
|
25
|
+
* @returns {number|null|undefined} Integer ID or null/undefined
|
|
26
|
+
* @throws {Error} If ID cannot be converted to integer
|
|
27
|
+
*/
|
|
28
|
+
_convertId(id) {
|
|
29
|
+
if (id === null || id === undefined) return id;
|
|
30
|
+
const parsed = parseInt(id, 10);
|
|
31
|
+
if (isNaN(parsed)) {
|
|
32
|
+
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert sync object IDs to strings
|
|
39
|
+
* @private
|
|
40
|
+
* @param {Object|null} sync - Sync object from database
|
|
41
|
+
* @returns {Object|null} Sync with string IDs
|
|
42
|
+
*/
|
|
43
|
+
_convertSyncIds(sync) {
|
|
44
|
+
if (!sync) return sync;
|
|
45
|
+
return {
|
|
46
|
+
...sync,
|
|
47
|
+
id: sync.id?.toString(),
|
|
48
|
+
integrationId: sync.integrationId?.toString(),
|
|
49
|
+
entities: sync.entities?.map(e => ({
|
|
50
|
+
...e,
|
|
51
|
+
id: e.id?.toString(),
|
|
52
|
+
userId: e.userId?.toString(),
|
|
53
|
+
credentialId: e.credentialId?.toString()
|
|
54
|
+
})),
|
|
55
|
+
dataIdentifiers: sync.dataIdentifiers?.map(di => ({
|
|
56
|
+
...di,
|
|
57
|
+
id: di.id?.toString(),
|
|
58
|
+
syncId: di.syncId?.toString(),
|
|
59
|
+
entityId: di.entityId?.toString(),
|
|
60
|
+
entity: di.entity ? {
|
|
61
|
+
...di.entity,
|
|
62
|
+
id: di.entity.id?.toString(),
|
|
63
|
+
userId: di.entity.userId?.toString(),
|
|
64
|
+
credentialId: di.entity.credentialId?.toString()
|
|
65
|
+
} : di.entity
|
|
66
|
+
}))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get a sync object by name, data identifier, and entity
|
|
72
|
+
*
|
|
73
|
+
* @param {string} name - The sync object name
|
|
74
|
+
* @param {Object} dataIdentifier - The data identifier object
|
|
75
|
+
* @param {string} entity - The entity ID (string from application layer)
|
|
76
|
+
* @returns {Promise<Object|null>} The sync object with string IDs or null
|
|
77
|
+
*/
|
|
78
|
+
async getSyncObject(name, dataIdentifier, entity) {
|
|
79
|
+
const intEntityId = this._convertId(entity);
|
|
80
|
+
const syncList = await this.prisma.sync.findMany({
|
|
81
|
+
where: {
|
|
82
|
+
name,
|
|
83
|
+
dataIdentifiers: {
|
|
84
|
+
some: {
|
|
85
|
+
idData: dataIdentifier,
|
|
86
|
+
entityId: intEntityId,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
include: {
|
|
91
|
+
entities: true,
|
|
92
|
+
dataIdentifiers: {
|
|
93
|
+
include: {
|
|
94
|
+
entity: true,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (syncList.length === 1) {
|
|
101
|
+
return this._convertSyncIds(syncList[0]);
|
|
102
|
+
} else if (syncList.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`There are multiple sync objects with the name ${name}, for entities [${syncList[0].entities}] [${syncList[1].entities}]`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create or update a sync object
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} filter - Filter criteria for finding existing sync
|
|
115
|
+
* @param {Object} syncData - Sync data to create/update (with string IDs from application layer)
|
|
116
|
+
* @returns {Promise<Object>} The created or updated sync object with string IDs
|
|
117
|
+
*/
|
|
118
|
+
async upsertSync(filter, syncData) {
|
|
119
|
+
// Find existing sync
|
|
120
|
+
const where = this._convertFilterToWhere(filter);
|
|
121
|
+
const existing = await this.prisma.sync.findFirst({ where });
|
|
122
|
+
|
|
123
|
+
// Convert IDs in syncData if present
|
|
124
|
+
const convertedData = { ...syncData };
|
|
125
|
+
if (convertedData.integrationId) {
|
|
126
|
+
convertedData.integrationId = this._convertId(convertedData.integrationId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (existing) {
|
|
130
|
+
// Update existing
|
|
131
|
+
const updated = await this.prisma.sync.update({
|
|
132
|
+
where: { id: existing.id },
|
|
133
|
+
data: convertedData,
|
|
134
|
+
});
|
|
135
|
+
return this._convertSyncIds(updated);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create new
|
|
139
|
+
const created = await this.prisma.sync.create({
|
|
140
|
+
data: convertedData,
|
|
141
|
+
});
|
|
142
|
+
return this._convertSyncIds(created);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update a sync object by ID
|
|
147
|
+
*
|
|
148
|
+
* @param {string} id - The sync object ID (string from application layer)
|
|
149
|
+
* @param {Object} updates - Updates to apply (with string IDs from application layer)
|
|
150
|
+
* @returns {Promise<Object>} The updated sync object with string IDs
|
|
151
|
+
*/
|
|
152
|
+
async updateSync(id, updates) {
|
|
153
|
+
const intId = this._convertId(id);
|
|
154
|
+
|
|
155
|
+
// Convert IDs in updates if present
|
|
156
|
+
const convertedUpdates = { ...updates };
|
|
157
|
+
if (convertedUpdates.integrationId) {
|
|
158
|
+
convertedUpdates.integrationId = this._convertId(convertedUpdates.integrationId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const updated = await this.prisma.sync.update({
|
|
162
|
+
where: { id: intId },
|
|
163
|
+
data: convertedUpdates,
|
|
164
|
+
});
|
|
165
|
+
return this._convertSyncIds(updated);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Add a data identifier to a sync object
|
|
170
|
+
*
|
|
171
|
+
* @param {string} syncId - The sync object ID (string from application layer)
|
|
172
|
+
* @param {Object} dataIdentifier - The data identifier to add (with string entity ID)
|
|
173
|
+
* @returns {Promise<Object>} The updated sync object with string IDs
|
|
174
|
+
*/
|
|
175
|
+
async addDataIdentifier(syncId, dataIdentifier) {
|
|
176
|
+
const intSyncId = this._convertId(syncId);
|
|
177
|
+
const intEntityId = this._convertId(dataIdentifier.entity);
|
|
178
|
+
|
|
179
|
+
// In Prisma, we create a new DataIdentifier record linked to the Sync
|
|
180
|
+
await this.prisma.dataIdentifier.create({
|
|
181
|
+
data: {
|
|
182
|
+
syncId: intSyncId,
|
|
183
|
+
entityId: intEntityId,
|
|
184
|
+
idData: dataIdentifier.id,
|
|
185
|
+
hash: dataIdentifier.hash,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Return updated sync object
|
|
190
|
+
const sync = await this.prisma.sync.findUnique({
|
|
191
|
+
where: { id: intSyncId },
|
|
192
|
+
include: {
|
|
193
|
+
dataIdentifiers: true,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
return this._convertSyncIds(sync);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get entity object ID for entity ID from sync object
|
|
201
|
+
*
|
|
202
|
+
* This is a pure helper method (no database access)
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} syncObj - The sync object (with string IDs from application layer)
|
|
205
|
+
* @param {string} entityId - The entity ID (string from application layer)
|
|
206
|
+
* @returns {Object} The entity object ID
|
|
207
|
+
*/
|
|
208
|
+
getEntityObjIdForEntityIdFromObject(syncObj, entityId) {
|
|
209
|
+
if (!syncObj.dataIdentifiers) {
|
|
210
|
+
throw new Error('Sync object must include dataIdentifiers');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (let dataIdentifier of syncObj.dataIdentifiers) {
|
|
214
|
+
// Compare string IDs (both should be strings at this point)
|
|
215
|
+
if (dataIdentifier.entityId === entityId) {
|
|
216
|
+
return dataIdentifier.idData;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Sync object ${syncObj.id} does not contain a data identifier for entity ${entityId}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Find sync objects by filter
|
|
227
|
+
*
|
|
228
|
+
* @param {Object} filter - Filter criteria (with string IDs from application layer)
|
|
229
|
+
* @returns {Promise<Array>} Array of sync objects with string IDs
|
|
230
|
+
*/
|
|
231
|
+
async findSyncs(filter) {
|
|
232
|
+
const where = this._convertFilterToWhere(filter);
|
|
233
|
+
const syncs = await this.prisma.sync.findMany({
|
|
234
|
+
where,
|
|
235
|
+
include: {
|
|
236
|
+
entities: true,
|
|
237
|
+
dataIdentifiers: {
|
|
238
|
+
include: {
|
|
239
|
+
entity: true,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return syncs.map(sync => this._convertSyncIds(sync));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find one sync object by filter
|
|
249
|
+
*
|
|
250
|
+
* @param {Object} filter - Filter criteria (with string IDs from application layer)
|
|
251
|
+
* @returns {Promise<Object|null>} The sync object with string IDs or null
|
|
252
|
+
*/
|
|
253
|
+
async findOneSync(filter) {
|
|
254
|
+
const where = this._convertFilterToWhere(filter);
|
|
255
|
+
const sync = await this.prisma.sync.findFirst({
|
|
256
|
+
where,
|
|
257
|
+
include: {
|
|
258
|
+
entities: true,
|
|
259
|
+
dataIdentifiers: {
|
|
260
|
+
include: {
|
|
261
|
+
entity: true,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
return this._convertSyncIds(sync);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Delete a sync object by ID
|
|
271
|
+
*
|
|
272
|
+
* @param {string} id - The sync object ID (string from application layer)
|
|
273
|
+
* @returns {Promise<Object>} The deletion result with string IDs
|
|
274
|
+
*/
|
|
275
|
+
async deleteSync(id) {
|
|
276
|
+
const intId = this._convertId(id);
|
|
277
|
+
// Prisma will cascade delete dataIdentifiers automatically
|
|
278
|
+
const deleted = await this.prisma.sync.delete({
|
|
279
|
+
where: { id: intId },
|
|
280
|
+
});
|
|
281
|
+
return this._convertSyncIds(deleted);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convert Mongoose-style filter to Prisma where clause (converting IDs to Int)
|
|
286
|
+
* @private
|
|
287
|
+
* @param {Object} filter - Mongoose filter (with string IDs from application layer)
|
|
288
|
+
* @returns {Object} Prisma where clause (with Int IDs for PostgreSQL)
|
|
289
|
+
*/
|
|
290
|
+
_convertFilterToWhere(filter) {
|
|
291
|
+
const where = {};
|
|
292
|
+
|
|
293
|
+
// Handle _id field (Mongoose uses _id, Prisma uses id)
|
|
294
|
+
if (filter._id) {
|
|
295
|
+
where.id = this._convertId(filter._id);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Handle id field
|
|
299
|
+
if (filter.id) {
|
|
300
|
+
where.id = this._convertId(filter.id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Handle integrationId field
|
|
304
|
+
if (filter.integrationId) {
|
|
305
|
+
where.integrationId = this._convertId(filter.integrationId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Handle integration field (Mongoose uses integration, Prisma uses integrationId)
|
|
309
|
+
if (filter.integration) {
|
|
310
|
+
where.integrationId = this._convertId(filter.integration);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Copy non-ID fields
|
|
314
|
+
const { _id, id, integrationId, integration, ...rest } = filter;
|
|
315
|
+
return { ...where, ...rest };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { SyncRepositoryPostgres };
|
package/syncs/sync.js
CHANGED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Frigg Telemetry & Usage Tracking
|
|
2
|
+
|
|
3
|
+
Vendor-neutral observability (traces + metrics) and durable, per-integration
|
|
4
|
+
feature-usage counters for `@friggframework/core`, built on OpenTelemetry.
|
|
5
|
+
Implements [ADR-011](../../../docs/architecture-decisions/011-integration-telemetry-and-usage-tracking.md).
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Two capabilities ride the same primitive:
|
|
10
|
+
|
|
11
|
+
1. **Observability** — spans + low-cardinality metrics across handlers, API
|
|
12
|
+
modules, queues and webhooks, per integration, exported to any OTLP backend
|
|
13
|
+
(Honeycomb, Datadog, Grafana, an OTel Collector, …).
|
|
14
|
+
2. **Usage tracking** — durable per-integration counters (records synced,
|
|
15
|
+
webhooks received, API requests, user actions, …) folded into a Frigg-owned
|
|
16
|
+
store that the reporting endpoint reads for apples-to-apples comparison.
|
|
17
|
+
|
|
18
|
+
### Key properties
|
|
19
|
+
|
|
20
|
+
- **Rides for free.** Framework seams are auto-instrumented — integrations get
|
|
21
|
+
handler/API-module/webhook metrics with zero code.
|
|
22
|
+
- **No-op by default.** With no exporter configured the service emits nothing and
|
|
23
|
+
loads **zero** OpenTelemetry modules — no cold-start cost. Integration code can
|
|
24
|
+
always call `this.telemetry.*`.
|
|
25
|
+
- **Vendor-neutral.** Integration code never imports a backend SDK. Swap exporters
|
|
26
|
+
in the app definition.
|
|
27
|
+
- **Usage store ≠ APM.** Reports read the durable Frigg store, never an external
|
|
28
|
+
APM.
|
|
29
|
+
|
|
30
|
+
## Configuration (app definition)
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// backend/index.js
|
|
34
|
+
const Definition = {
|
|
35
|
+
name: 'my-app',
|
|
36
|
+
integrations: [HubSpotIntegration, SalesforceIntegration],
|
|
37
|
+
|
|
38
|
+
telemetry: {
|
|
39
|
+
// none | console | otlp | honeycomb | datadog
|
|
40
|
+
exporter: {
|
|
41
|
+
type: 'otlp',
|
|
42
|
+
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
43
|
+
// headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY }, // or use type:'honeycomb' + apiKey
|
|
44
|
+
},
|
|
45
|
+
sampleRatio: 1.0, // parent-based trace sampling (0..1)
|
|
46
|
+
northStar: {
|
|
47
|
+
default: { name: 'records.synced' },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Exporter default (no `telemetry.exporter` set):** `console` only when
|
|
54
|
+
`STAGE=local` (a genuinely local run); every deployed stage — including `dev` —
|
|
55
|
+
defaults to `none` (no per-event cost, no data written to CloudWatch). Point
|
|
56
|
+
`exporter` at an OTLP backend to turn export on.
|
|
57
|
+
|
|
58
|
+
**Sampling (`sampleRatio`, `0..1`, default `1`):** the fraction of **traces**
|
|
59
|
+
exported — a cost knob for high-traffic fleets (`0.1` ≈ keep 10%). Whole traces
|
|
60
|
+
are sampled (trace-ID-based + parent-based, so a distributed trace is never
|
|
61
|
+
half-kept), and it does **not** thin the durable **usage counters** — those stay
|
|
62
|
+
exact at any ratio (they ride the event bus, not the sampled trace pipeline). It
|
|
63
|
+
is **not** error-aware: a low ratio drops failed-run traces too, so for "keep all
|
|
64
|
+
errors, sample the rest" use tail-based sampling at an OTel Collector, not this
|
|
65
|
+
knob. Typical: `1.0` in dev, lower (e.g. `0.1`) in high-volume prod.
|
|
66
|
+
|
|
67
|
+
### Environment variables
|
|
68
|
+
|
|
69
|
+
| Variable | Purpose |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP backend URL (referenced from `telemetry.exporter.endpoint`). Auto-passed through to Lambda when an OTLP-family exporter is configured. |
|
|
72
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | OTLP headers (e.g. auth). |
|
|
73
|
+
| `OTEL_FLUSH_TIMEOUT_MS` | Max time the handler waits to flush telemetry before returning (default `500`). Bounds tail latency if the backend is unreachable. |
|
|
74
|
+
| `OTEL_METRIC_EXPORT_INTERVAL_MS` | Metric reader interval (default `60000`). |
|
|
75
|
+
| `STAGE` | `local` enables the `console` default. |
|
|
76
|
+
|
|
77
|
+
> **VPC note:** a Lambda in a private subnet needs a NAT gateway or VPC endpoint
|
|
78
|
+
> to reach an external OTLP backend. Without egress the exporter fails silently
|
|
79
|
+
> within `OTEL_FLUSH_TIMEOUT_MS`.
|
|
80
|
+
|
|
81
|
+
## What you get for free (auto-instrumentation)
|
|
82
|
+
|
|
83
|
+
No integration code required — every emission carries `{integration_type, event,
|
|
84
|
+
status, ...}` bounded labels, with high-cardinality ids on span baggage / the bus
|
|
85
|
+
context only.
|
|
86
|
+
|
|
87
|
+
| Seam | Span | Metric |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| Handler dispatch (USER_ACTION / CRON / QUEUE / WEBHOOK / lifecycle) | `frigg.handler.<type>` | `frigg.handler.invocations{integration_type, event, status}` |
|
|
90
|
+
| Outbound API-module request | `frigg.apimodule.request` | `frigg.apimodule.requests{module, method, status}` |
|
|
91
|
+
|
|
92
|
+
Request URLs are redacted (query string + userinfo stripped) before they touch a
|
|
93
|
+
span, so credentials in query params never leak.
|
|
94
|
+
|
|
95
|
+
> **Usage-attribution boundary.** The OTel metrics above fire for *every* seam
|
|
96
|
+
> invocation. The durable per-integration **usage** rollup, though, only counts
|
|
97
|
+
> emissions that carry an integration context — set by the handler seams. Requests
|
|
98
|
+
> an API module makes *before an integration exists* (OAuth/token exchange, entity
|
|
99
|
+
> discovery during connection setup) are observable in traces but not attributed to
|
|
100
|
+
> an `api.requests` usage counter (there is no integration to attribute them to).
|
|
101
|
+
|
|
102
|
+
## Custom metrics (integration code)
|
|
103
|
+
|
|
104
|
+
Every integration instance carries `this.telemetry` (auto-tagged with its
|
|
105
|
+
`integration_type`):
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
class HubSpotIntegration extends IntegrationBase {
|
|
109
|
+
async deltaSync() {
|
|
110
|
+
// A span wraps the operation (nested under the handler span):
|
|
111
|
+
await this.telemetry.span('delta_sync', async () => {
|
|
112
|
+
const batch = await this.hubspot.api.getContacts();
|
|
113
|
+
|
|
114
|
+
// A counter — declare 'records.synced' in Definition.usage to persist it:
|
|
115
|
+
this.telemetry.count('records.synced', batch.length, {
|
|
116
|
+
entity: 'contact',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// A one-off event (attached to the active span):
|
|
120
|
+
this.telemetry.event('workflow_invoked', { workflow: 'lead_route' });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Usage counters (durable, comparable)
|
|
127
|
+
|
|
128
|
+
### 1. Declare which counters an integration reports
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
class HubSpotIntegration extends IntegrationBase {
|
|
132
|
+
static Definition = {
|
|
133
|
+
name: 'hubspot',
|
|
134
|
+
usage: {
|
|
135
|
+
// Canonical keys → comparable ACROSS integration types (reporting):
|
|
136
|
+
canonical: ['records.synced', 'webhooks.received', 'api.requests'],
|
|
137
|
+
// Custom keys → comparable only WITHIN this integration type:
|
|
138
|
+
custom: { 'deals.enriched': { unit: 'count', label: 'Deals enriched' } },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Canonical vocabulary** (core-owned, versioned):
|
|
145
|
+
|
|
146
|
+
| Key | Source |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `api.requests` | auto — every outbound API-module request |
|
|
149
|
+
| `user_actions` | auto — every `USER_ACTION` handler |
|
|
150
|
+
| `webhooks.received` | auto — the `ON_WEBHOOK` queue dispatch (per-integration, DB-connected) |
|
|
151
|
+
| `records.synced` | explicit — `this.telemetry.count('records.synced', n, { entity })` |
|
|
152
|
+
| `workflows.invoked` | explicit — `this.telemetry.count('workflows.invoked', 1, { workflow })` |
|
|
153
|
+
|
|
154
|
+
Only **declared** keys are persisted. Declaring a canonical key opts the
|
|
155
|
+
integration into the comparison report.
|
|
156
|
+
|
|
157
|
+
### 2. Read the usage store
|
|
158
|
+
|
|
159
|
+
`frigg.usage.*` (via `createFriggCommands`) reads the durable store — never an APM:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
const { createFriggCommands } = require('@friggframework/core');
|
|
163
|
+
const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
|
|
164
|
+
|
|
165
|
+
// Apples-to-apples comparison across integration types:
|
|
166
|
+
await frigg.usage.getTotalsByDimension({
|
|
167
|
+
metric: 'records.synced',
|
|
168
|
+
groupBy: 'integrationType', // or 'metric'
|
|
169
|
+
since: daysAgo(30),
|
|
170
|
+
bucket: 'day', // 'day' (default) | 'hour'
|
|
171
|
+
});
|
|
172
|
+
// → [{ integrationType: 'hubspot', value: 4200 }, { integrationType: 'salesforce', value: 1180 }]
|
|
173
|
+
|
|
174
|
+
// Trend series for one type (aggregated across its instances):
|
|
175
|
+
await frigg.usage.getTimeSeries({
|
|
176
|
+
metric: 'records.synced',
|
|
177
|
+
integrationType: 'hubspot',
|
|
178
|
+
from: daysAgo(7),
|
|
179
|
+
to: new Date(),
|
|
180
|
+
bucket: 'day',
|
|
181
|
+
});
|
|
182
|
+
// → [{ bucket: 'day:2026-07-04', value: 610 }, { bucket: 'day:2026-07-05', value: 720 }]
|
|
183
|
+
|
|
184
|
+
// Manual write (day + hour windows derived from `at`):
|
|
185
|
+
await frigg.usage.recordUsageCounter({
|
|
186
|
+
integrationId: 'int_1',
|
|
187
|
+
integrationType: 'hubspot',
|
|
188
|
+
metric: 'deals.enriched',
|
|
189
|
+
value: 3,
|
|
190
|
+
at: new Date(),
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The [reporting endpoint](../reporting/README.md) surfaces these as additive
|
|
195
|
+
`usage` columns on each `byType` bucket.
|
|
196
|
+
|
|
197
|
+
## North Star metric
|
|
198
|
+
|
|
199
|
+
Declare an adopter North Star that reports/snapshots read as a first-class
|
|
200
|
+
counter — populated by direct emission, or derived from a trace signal with no
|
|
201
|
+
integration code:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
telemetry: {
|
|
205
|
+
northStar: {
|
|
206
|
+
default: { name: 'records.synced' },
|
|
207
|
+
byType: {
|
|
208
|
+
hubspot: {
|
|
209
|
+
name: 'contacts_synced',
|
|
210
|
+
// derive from an auto-emitted signal:
|
|
211
|
+
deriveFrom: { apiRequest: { endpoint: '/contacts', method: 'POST' } },
|
|
212
|
+
// or: deriveFrom: { userAction: { action: 'route_lead' } }
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Read it as a first-class metric without knowing the configured key — the North
|
|
220
|
+
Star resolves per integration type (`byType` wins over `default`):
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
// The caller passes the North Star config it already holds (from the app
|
|
224
|
+
// definition); this resolves the counter for the type and returns its totals.
|
|
225
|
+
await frigg.usage.getNorthStarTotals({
|
|
226
|
+
northStar: definition.telemetry.northStar,
|
|
227
|
+
integrationType: 'hubspot',
|
|
228
|
+
since: daysAgo(30),
|
|
229
|
+
});
|
|
230
|
+
// → { metric: 'contacts_synced', totals: [{ integrationType: 'hubspot', value: 900 }] }
|
|
231
|
+
// → null when `northStar` is absent or has no entry for the type
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or read it like any counter once you know the key:
|
|
235
|
+
`frigg.usage.getTotalsByDimension({ metric: 'contacts_synced' })`; trends via `frigg.usage.getTimeSeries({ metric })`.
|
|
236
|
+
|
|
237
|
+
> The North Star read takes its config as a **call argument** — nothing
|
|
238
|
+
> telemetry-specific is threaded through `createFriggCommands`. The caller that
|
|
239
|
+
> owns the app definition (e.g. a report runner) passes `telemetry.northStar` in;
|
|
240
|
+
> omit it and `getNorthStarTotals()` returns `null`.
|
|
241
|
+
|
|
242
|
+
## Plugin / extension tap
|
|
243
|
+
|
|
244
|
+
Telemetry flows onto an internal event stream (independent of OTel export, so
|
|
245
|
+
taps fire even with `exporter: none`). Two ways to subscribe:
|
|
246
|
+
|
|
247
|
+
**Declarative (app definition)** — the framework wires these once per cold start,
|
|
248
|
+
each guarded so a bad subscriber can't break emission or its siblings:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
// backend/index.js
|
|
252
|
+
const Definition = {
|
|
253
|
+
telemetry: {
|
|
254
|
+
subscribers: [
|
|
255
|
+
// (a) declarative object — `event` optional; omit to receive both:
|
|
256
|
+
{ event: 'metric', handler: ({ name, value, attributes, context }) => {
|
|
257
|
+
forwardToStatsd(name, value, attributes);
|
|
258
|
+
} },
|
|
259
|
+
// (b) factory — gets the telemetry service, registers itself, may
|
|
260
|
+
// return an unsubscribe:
|
|
261
|
+
(telemetry) => telemetry.on('event', (payload) => auditSink.write(payload)),
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Imperative** — subscribe from anywhere that runs at startup:
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const { getTelemetry } = require('@friggframework/core');
|
|
271
|
+
|
|
272
|
+
const off = getTelemetry().on('metric', ({ name, value, attributes, context }) => {
|
|
273
|
+
// `attributes` = bounded metric labels; `context` = high-cardinality ids
|
|
274
|
+
// (integrationId, integrationType, userId, url, …). Never throws upstream.
|
|
275
|
+
});
|
|
276
|
+
// off() to unsubscribe
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
> The bus payload shape (`{ name, value, attributes, context? }` for `'metric'`,
|
|
280
|
+
> `{ name, attributes, context? }` for `'event'`) is a stable contract.
|
|
281
|
+
|
|
282
|
+
## Cardinality rule
|
|
283
|
+
|
|
284
|
+
High-cardinality identifiers (`integrationId`, `userId`, request `url`, action
|
|
285
|
+
names) ride **span baggage / the bus `context`** — never OTel **metric** labels.
|
|
286
|
+
Metric labels stay bounded (`integration_type`, `event`, `status`, `method`,
|
|
287
|
+
`module`). The usage rollup derives per-integration counts from the bus context,
|
|
288
|
+
not from metric labels.
|
|
289
|
+
|
|
290
|
+
## How it works
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
this.telemetry.count / auto-instrumented seam
|
|
294
|
+
│ (bounded metric labels → OTel; ids → bus context via AsyncLocalStorage)
|
|
295
|
+
├──────────────► OTel exporter (traces + metrics) [observability]
|
|
296
|
+
└──────────────► TelemetryEventBus ('metric'/'event')
|
|
297
|
+
│
|
|
298
|
+
├─ UsageRollupSubscriber ── buffers per invocation,
|
|
299
|
+
│ flushes to the UsageCounter store on handler exit
|
|
300
|
+
│ (discards on SQS redelivery — approximate contract)
|
|
301
|
+
└─ your plugin taps
|
|
302
|
+
|
|
303
|
+
frigg.usage.getTotalsByDimension / getTimeSeries ◄── UsageCounter store ──► reporting usage columns
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- **Flush is Lambda-safe:** `create-handler` awaits a bounded `forceFlush` in a
|
|
307
|
+
`finally` (background timers can't fire once the container freezes).
|
|
308
|
+
- **Usage accuracy is approximate:** at-least-once delivery means a retried
|
|
309
|
+
handler could double-count. The invocation buffer is discarded only when the
|
|
310
|
+
**whole** SQS batch is a redelivery (`ApproximateReceiveCount > 1`); a mixed
|
|
311
|
+
batch flushes so a redelivered sibling never drops a fresh record's counts.
|
|
312
|
+
|
|
313
|
+
## Caveats / current limitations
|
|
314
|
+
|
|
315
|
+
- **Usage persistence requires a DB-connected handler.** DB-free handlers (e.g.
|
|
316
|
+
the raw webhook-receipt route) can't write; `webhooks.received` is counted at
|
|
317
|
+
the DB-connected `ON_WEBHOOK` queue dispatch instead.
|
|
318
|
+
- **DocumentDB** uses a raw-command adapter (`$runCommandRaw`) for increment and
|
|
319
|
+
aggregate; command shapes are unit-tested but not yet run against a real cluster.
|
|
320
|
+
- **Retention:** the `UsageCounter` table has no pruning yet — hour-grain rows
|
|
321
|
+
accumulate. Add a scheduled prune for high-volume deployments. (Read paths are
|
|
322
|
+
covered by composite indexes `(metric, window)` and `(metric, integrationType,
|
|
323
|
+
window)`.)
|
|
324
|
+
- Metric `value` is a `BigInt` per `(integrationId, integrationType, metric,
|
|
325
|
+
window)` row; reads coerce the sum to a JS Number (safe below 2^53).
|
|
326
|
+
|
|
327
|
+
## See also
|
|
328
|
+
|
|
329
|
+
- Architecture: [ADR-011](../../../docs/architecture-decisions/011-integration-telemetry-and-usage-tracking.md)
|
|
330
|
+
- Reporting hand-off: [`reporting/README.md`](../reporting/README.md)
|
|
331
|
+
- Encryption (same repository-triad pattern): [`database/encryption/README.md`](../database/encryption/README.md)
|