├── .eslintrc.js ├── .gitconfig ├── .github ├── renovate.json └── workflows │ ├── merge-main.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .syncpackrc.js ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── commitlint.config.js ├── commonConfiguration ├── .vscode │ └── launch.json └── dependency-cruiser.config.js ├── contracts └── construct-contracts │ ├── .dependency-cruiser.cjs │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .lintstagedrc.cjs │ ├── .vscode │ ├── package.json │ ├── project.json │ ├── src │ ├── entities │ │ ├── eventPattern.ts │ │ └── index.ts │ ├── index.ts │ ├── requests │ │ ├── index.ts │ │ ├── listEvents.ts │ │ ├── startEventsTrail.ts │ │ └── stopEventsTrail.ts │ └── websocket │ │ ├── index.ts │ │ └── startTrail.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── event-scout.code-workspace ├── lerna.json ├── nx.json ├── package.json ├── packages ├── client │ ├── .dependency-cruiser.cjs │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .lintstagedrc.cjs │ ├── .vscode │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── EventScoutClient.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── construct │ ├── .dependency-cruiser.cjs │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .lintstagedrc.cjs │ ├── .vscode │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── EventScout.ts │ │ ├── httpApiTrail │ │ │ ├── functions │ │ │ │ ├── storeEvents.ts │ │ │ │ └── trailGarbageCollector.ts │ │ │ └── httpApiTrail.ts │ │ ├── index.ts │ │ └── webSocketTrail │ │ │ ├── functions │ │ │ ├── forwardEvent.ts │ │ │ ├── onStartTrail.ts │ │ │ ├── onWebSocketConnect.ts │ │ │ └── onWebSocketDisconnect.ts │ │ │ └── webSocketTrail.ts │ ├── tests │ │ └── stack.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── event-scout │ ├── .dependency-cruiser.js │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── .vscode │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── buildArguments.ts │ │ ├── index.ts │ │ └── listenToWebSocket.ts │ ├── tsconfig.json │ └── vitest.config.mts └── lambda-assets │ ├── .dependency-cruiser.cjs │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .lintstagedrc.cjs │ ├── README.md │ ├── esbuild.build.js │ ├── package.json │ ├── project.json │ ├── src │ ├── forwardEvent.ts │ ├── listEvents.ts │ ├── onStartTrail.ts │ ├── onWebSocketConnect.ts │ ├── onWebSocketDisconnect.ts │ ├── startEventsTrail.ts │ ├── stopEventsTrail.ts │ ├── storeEvents.ts │ ├── trailGarbageCollector.ts │ └── utils │ │ ├── createEventBridgeRuleAndTarget.ts │ │ ├── deleteEventBridgeRuleAndTarget.ts │ │ ├── getRuleAndTargetName.ts │ │ └── listAllTrailEvents.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── services └── validation │ ├── .dependency-cruiser.cjs │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .lintstagedrc.cjs │ ├── .vscode │ ├── cdk.json │ ├── global.d.ts │ ├── integrationTests │ └── restApiIntegration.test.ts │ ├── package.json │ ├── project.json │ ├── resources │ ├── index.ts │ └── stack.ts │ ├── tsconfig.json │ ├── utils │ ├── exportNames.ts │ └── sharedConfig.ts │ ├── vite.config-integration.ts │ ├── vite.config.ts │ └── vitestIntegrationSetup.ts ├── tsconfig.json └── tsconfig.options.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-lines */ 2 | module.exports = { 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:import/recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | rules: { 9 | 'prettier/prettier': 'error', 10 | 'import/extensions': 0, 11 | 'import/no-unresolved': 0, 12 | 'import/prefer-default-export': 0, 13 | 'import/no-duplicates': 'error', 14 | complexity: ['error', 8], 15 | 'max-lines': ['error', 200], 16 | 'max-depth': ['error', 3], 17 | 'max-params': ['error', 4], 18 | eqeqeq: ['error', 'smart'], 19 | 'import/no-extraneous-dependencies': [ 20 | 'error', 21 | { 22 | devDependencies: true, 23 | optionalDependencies: false, 24 | peerDependencies: false, 25 | }, 26 | ], 27 | 'no-shadow': [ 28 | 'error', 29 | { 30 | hoist: 'all', 31 | }, 32 | ], 33 | 'prefer-const': 'error', 34 | 'import/order': [ 35 | 'error', 36 | { 37 | pathGroups: [{ pattern: '@event-scout/**', group: 'unknown' }], 38 | groups: [ 39 | ['external', 'builtin'], 40 | 'unknown', 41 | 'internal', 42 | ['parent', 'sibling', 'index'], 43 | ], 44 | alphabetize: { 45 | order: 'asc', 46 | caseInsensitive: false, 47 | }, 48 | 'newlines-between': 'always', 49 | pathGroupsExcludedImportTypes: ['builtin'], 50 | }, 51 | ], 52 | 'sort-imports': [ 53 | 'error', 54 | { 55 | ignoreCase: true, 56 | ignoreDeclarationSort: true, 57 | ignoreMemberSort: false, 58 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 59 | }, 60 | ], 61 | 'padding-line-between-statements': [ 62 | 'error', 63 | { 64 | blankLine: 'always', 65 | prev: '*', 66 | next: 'return', 67 | }, 68 | ], 69 | 'prefer-arrow/prefer-arrow-functions': [ 70 | 'error', 71 | { 72 | disallowPrototype: true, 73 | singleReturnOnly: false, 74 | classPropertiesAllowed: false, 75 | }, 76 | ], 77 | 'no-restricted-imports': [ 78 | 'error', 79 | { 80 | patterns: [ 81 | { 82 | group: ['@event-scout/*/*'], 83 | message: 84 | 'import of internal modules must be done at the root level.', 85 | }, 86 | ], 87 | paths: [ 88 | { 89 | name: 'lodash', 90 | message: 'Please use lodash/{module} import instead', 91 | }, 92 | { 93 | name: 'aws-sdk', 94 | message: 'Please use aws-sdk/{module} import instead', 95 | }, 96 | { 97 | name: '.', 98 | message: 'Please use explicit import file', 99 | }, 100 | ], 101 | }, 102 | ], 103 | curly: ['error', 'all'], 104 | }, 105 | root: true, 106 | env: { 107 | es6: true, 108 | node: true, 109 | browser: true, 110 | }, 111 | plugins: ['prefer-arrow', 'import'], 112 | parserOptions: { 113 | ecmaVersion: 'latest', 114 | sourceType: 'module', 115 | }, 116 | overrides: [ 117 | { 118 | files: ['**/*.ts?(x)'], 119 | extends: [ 120 | 'plugin:@typescript-eslint/recommended', 121 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 122 | 'plugin:prettier/recommended', 123 | ], 124 | parser: '@typescript-eslint/parser', 125 | parserOptions: { 126 | project: 'tsconfig.json', 127 | }, 128 | rules: { 129 | '@typescript-eslint/prefer-optional-chain': 'error', 130 | 'no-shadow': 'off', 131 | '@typescript-eslint/no-shadow': 'error', 132 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 133 | '@typescript-eslint/strict-boolean-expressions': [ 134 | 'error', 135 | { 136 | allowString: false, 137 | allowNumber: false, 138 | allowNullableObject: true, 139 | }, 140 | ], 141 | '@typescript-eslint/ban-ts-comment': [ 142 | 'error', 143 | { 144 | 'ts-ignore': 'allow-with-description', 145 | minimumDescriptionLength: 10, 146 | }, 147 | ], 148 | '@typescript-eslint/explicit-function-return-type': 0, 149 | '@typescript-eslint/explicit-member-accessibility': 0, 150 | '@typescript-eslint/camelcase': 0, 151 | '@typescript-eslint/interface-name-prefix': 0, 152 | '@typescript-eslint/explicit-module-boundary-types': 'error', 153 | '@typescript-eslint/no-explicit-any': 'error', 154 | '@typescript-eslint/no-unused-vars': 'error', 155 | '@typescript-eslint/ban-types': [ 156 | 'error', 157 | { 158 | types: { 159 | FC: 'Use `const MyComponent = (props: Props): JSX.Element` instead', 160 | SFC: 'Use `const MyComponent = (props: Props): JSX.Element` instead', 161 | FunctionComponent: 162 | 'Use `const MyComponent = (props: Props): JSX.Element` instead', 163 | 'React.FC': 164 | 'Use `const MyComponent = (props: Props): JSX.Element` instead', 165 | 'React.SFC': 166 | 'Use `const MyComponent = (props: Props): JSX.Element` instead', 167 | 'React.FunctionComponent': 168 | 'Use `const MyComponent = (props: Props): JSX.Element` instead', 169 | }, 170 | extendDefaults: true, 171 | }, 172 | ], 173 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', 174 | '@typescript-eslint/no-unnecessary-condition': 'error', 175 | '@typescript-eslint/no-unnecessary-type-arguments': 'error', 176 | '@typescript-eslint/prefer-string-starts-ends-with': 'error', 177 | '@typescript-eslint/switch-exhaustiveness-check': 'error', 178 | '@typescript-eslint/restrict-template-expressions': [ 179 | 'error', 180 | { 181 | allowNumber: true, 182 | allowBoolean: true, 183 | }, 184 | ], 185 | }, 186 | }, 187 | { 188 | files: ['**/src/**'], 189 | excludedFiles: ['**/__tests__/**', '**/*.test.ts?(x)'], 190 | rules: { 191 | 'import/no-extraneous-dependencies': [ 192 | 'error', 193 | { 194 | devDependencies: false, 195 | optionalDependencies: false, 196 | peerDependencies: true, 197 | }, 198 | ], 199 | }, 200 | }, 201 | ], 202 | }; 203 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [pull] 2 | rebase = true 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "packageRules": [ 5 | { 6 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 7 | "automerge": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/merge-main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags-ignore: 6 | - 'v*' # exclude version tags 7 | 8 | name: 🚀 Merge main 9 | concurrency: merge_main 10 | 11 | permissions: 12 | id-token: write # this is required for AWS https://github.com/aws-actions/configure-aws-credentials#usage 13 | contents: read # this is required for Nx https://github.com/nrwl/nx-set-shas#permissions-in-v2 14 | actions: read # this is required for Nx https://github.com/nrwl/nx-set-shas#permissions-in-v2 15 | 16 | env: 17 | CI: true 18 | AWS_REGION: eu-west-1 19 | 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | jobs: 25 | build-and-test: 26 | name: 🏗 Build Project, 🧪 Run Tests & 🚀 Deploy staging 27 | runs-on: ubuntu-latest 28 | environment: staging 29 | timeout-minutes: 30 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | # We need to fetch all branches and commits so that Nx affected has a base to compare against. 35 | fetch-depth: 0 36 | 37 | - name: Setup Nx SHAs 38 | uses: nrwl/nx-set-shas@v4 39 | 40 | - name: Setup pnpm 41 | uses: pnpm/action-setup@v4.1.0 42 | 43 | - name: Setup Node.js 44 | id: setup-node 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version-file: '.nvmrc' 48 | cache: 'pnpm' 49 | 50 | - name: Install dependencies 51 | run: pnpm install --frozen-lockfile 52 | 53 | - name: 💾 Cache Nx cache 54 | id: package-cache 55 | uses: actions/cache@v4 56 | with: 57 | path: | 58 | nx-cache 59 | # Cache will be updated at every run: https://github.com/actions/cache/blob/main/workarounds.md#update-a-cache 60 | key: ${{ runner.os }}-nx-cache-${{ steps.setup-node.outputs.node-version }}-${{ github.run_id }} 61 | 62 | - name: '🏗 Package' 63 | run: pnpm nx affected --targets=package,build --parallel=2 64 | 65 | - name: '🧪 Run tests' 66 | run: pnpm nx affected --targets=test-linter,test-type,test-unit,test-circular,test-cdk --parallel=2 67 | 68 | - name: Configure AWS Credentials 69 | uses: aws-actions/configure-aws-credentials@v4 70 | with: 71 | aws-region: ${{ env.AWS_REGION }} 72 | role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_STAGING }} 73 | 74 | - name: '🚀 Deploy staging' 75 | run: pnpm nx affected --target=deploy-staging --parallel=2 76 | 77 | - name: '🔎 Run integration tests on staging' 78 | run: SLS_ENV=staging pnpm nx affected --target=test-integration --parallel=2 79 | 80 | - name: '🧹 Clean Unused CDK assets' 81 | run: pnpm nx affected --target=cdk-gc 82 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: [opened, synchronize, reopened] 4 | 5 | # cancel previous runs on the same PR 6 | concurrency: 7 | group: ${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | name: ⛷ PR tests 11 | 12 | env: 13 | CI: true 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | jobs: 20 | build-and-test: 21 | name: 🏗 Build Project & 🧪 Run Tests 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 30 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | ref: ${{ github.event.pull_request.head.sha }} 28 | # We need to fetch all branches and commits so that Nx affected has a base to compare against. 29 | fetch-depth: 0 30 | - uses: nrwl/nx-set-shas@v4 31 | - uses: pnpm/action-setup@v4.1.0 32 | - name: Setup Node.js 33 | id: setup-node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version-file: '.nvmrc' 37 | cache: 'pnpm' 38 | - name: Install dependencies 39 | run: pnpm install --frozen-lockfile 40 | - name: 💾 Cache Nx cache 41 | id: package-cache 42 | uses: actions/cache@v4 43 | with: 44 | path: | 45 | nx-cache 46 | # Cache will be updated at every run: https://github.com/actions/cache/blob/main/workarounds.md#update-a-cache 47 | key: ${{ runner.os }}-nx-cache-${{ steps.setup-node.outputs.node-version }}-${{ github.run_id }} 48 | - name: '🏗 Package' 49 | run: pnpm nx affected --targets=package,build --parallel=2 50 | - name: '🧪 Run tests' 51 | run: pnpm nx affected --targets=test-linter,test-type,test-unit,test-circular,test-cdk --parallel=2 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: 🔖 Release 7 | concurrency: release 8 | 9 | permissions: 10 | id-token: write # this is required for AWS https://github.com/aws-actions/configure-aws-credentials#usage 11 | contents: write # this is required for Nx https://github.com/nrwl/nx-set-shas#permissions-in-v2 12 | actions: read # this is required for Nx https://github.com/nrwl/nx-set-shas#permissions-in-v2 13 | 14 | env: 15 | CI: true 16 | AWS_REGION: eu-west-1 17 | 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | jobs: 23 | release: 24 | name: 🔖 Release 25 | runs-on: ubuntu-latest 26 | environment: production 27 | timeout-minutes: 30 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | with: 32 | # We need to fetch all branches and commits so that Nx affected has a base to compare against. 33 | fetch-depth: 0 34 | 35 | - name: Setup Nx SHAs 36 | uses: nrwl/nx-set-shas@v4 37 | 38 | - name: Setup pnpm 39 | uses: pnpm/action-setup@v4.1.0 40 | 41 | - name: Setup Node.js 42 | id: setup-node 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version-file: '.nvmrc' 46 | cache: 'pnpm' 47 | - name: Install dependencies 48 | run: pnpm install --frozen-lockfile 49 | - name: 💾 Cache Nx cache 50 | id: package-cache 51 | uses: actions/cache@v4 52 | with: 53 | path: | 54 | nx-cache 55 | # Cache will be updated at every run: https://github.com/actions/cache/blob/main/workarounds.md#update-a-cache 56 | key: ${{ runner.os }}-nx-cache-${{ steps.setup-node.outputs.node-version }}-${{ github.run_id }} 57 | - name: '🏗 Package' 58 | run: pnpm nx affected --targets=package,build --parallel=2 59 | - name: '🧪 Run tests' 60 | run: pnpm nx affected --targets=test-linter,test-type,test-unit,test-circular,test-cdk --parallel=2 61 | - name: Configure AWS Credentials 62 | uses: aws-actions/configure-aws-credentials@v4 63 | with: 64 | aws-region: ${{ env.AWS_REGION }} 65 | role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_PRODUCTION }} 66 | 67 | - name: '🚀 Deploy production' 68 | run: pnpm nx affected --target=deploy-production --parallel=2 69 | 70 | - name: '🔎 Run integration tests on production' 71 | run: SLS_ENV=production pnpm nx affected --target=test-integration --parallel=2 72 | 73 | - name: '🧹 Clean Unused CDK assets' 74 | run: pnpm nx affected --target=cdk-gc 75 | 76 | - name: Get latest release matching release tag type 77 | id: get-latest-release 78 | uses: rez0n/actions-github-release@main 79 | with: 80 | token: ${{ secrets.GITHUB_TOKEN }} 81 | repository: ${{ github.repository }} 82 | # 'prerelease' if tag inclues 'alpha', otherwise 'stable' 83 | type: ${{ contains(github.ref, 'alpha') && 'prerelease' || 'stable' }} 84 | 85 | - name: Create draft Github release 86 | run: pnpm changelogithub --draft --from ${{ steps.get-latest-release.outputs.release }} 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | .npm 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | # Esbuild directories 12 | .esbuild 13 | 14 | # Ignore stack.json files 15 | **/stack.json 16 | 17 | # production 18 | /build 19 | **/dist 20 | 21 | # testing 22 | coverage 23 | 24 | # Ignore Jetbrains folder settings 25 | .idea 26 | 27 | # local env 28 | .env 29 | .env.dev 30 | .env.development 31 | .env.local 32 | 33 | # misc 34 | .DS_Store 35 | npm-debug.log* 36 | lerna-debug.log* 37 | 38 | # If people use virtualenvs for python scripts 39 | .venv 40 | 41 | # remove files to invoke lambdas locally 42 | event.json 43 | 44 | # Nx caching 45 | nx-cache 46 | .nx 47 | 48 | # Build cache manifests 49 | *.tsbuildinfo 50 | 51 | # CDK output files 52 | cdk.out 53 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,ts}': 'pnpm lint-fix', 3 | }; 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | git-checks=false 3 | publish-branch=main 4 | package-manager-strict=false 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.16.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.hbs 2 | .next 3 | pnpm-lock.yaml 4 | **/.serverless 5 | **/stack.json 6 | .gitlab-ci.yml 7 | .npm 8 | .webpack 9 | .esbuild 10 | **/coverage 11 | **/dist 12 | **/build 13 | **/public 14 | **/nx-cache 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.syncpackrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: true, 3 | filter: '.', 4 | indent: ' ', 5 | peer: true, 6 | prod: true, 7 | semverRange: '', 8 | sortAz: [ 9 | 'contributors', 10 | 'scripts', 11 | 'dependencies', 12 | 'devDependencies', 13 | 'keywords', 14 | 'peerDependencies', 15 | ], 16 | sortFirst: [ 17 | 'name', 18 | 'description', 19 | 'private', 20 | 'version', 21 | 'author', 22 | 'contributors', 23 | 'license', 24 | 'homepage', 25 | 'bugs', 26 | 'repository', 27 | 'keywords', 28 | 'publishConfig', 29 | 'workspaces', 30 | 'sideEffects', 31 | 'files', 32 | 'type', 33 | 'main', 34 | 'module', 35 | 'types', 36 | 'exports', 37 | 'contracts', 38 | 'generators', 39 | 'scripts', 40 | ], 41 | versionGroups: [], 42 | }; 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Install the project 4 | 5 | Get the correct NodeJS version: 6 | 7 | ```bash 8 | nvm use 9 | ``` 10 | 11 | Install pnpm: 12 | 13 | ```bash 14 | npm install -g pnpm 15 | ``` 16 | 17 | Install dependencies: 18 | 19 | ```bash 20 | pnpm install 21 | ``` 22 | 23 | ## Release 24 | 25 | Prepare: 26 | 27 | ```bash 28 | pnpm install 29 | pnpm package --skip-nx-cache 30 | pnpm build --skip-nx-cache 31 | ``` 32 | 33 | Version & publish: 34 | 35 | ```bash 36 | pnpm lerna version --force-publish 37 | pnpm publish --recursive --access public 38 | ``` 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021-2025 François Farge 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Event Scout 2 | 3 | Easy testing and monitoring for events flows on Amazon EventBridge. 4 | 5 | ## Why EventScout? 6 | 7 | Debugging and testing asynchronous flows on Amazon EventBridge is hard, especially since AWS does not provide a simple way to monitor what events have transited through an Event Bus. 8 | 9 | EventScout aims to provide this missing piece, both during development and in automated integration tests, by creating **event trails**. These trail record all events with a specific shape. You can then query the trail to see what events have transited through the bus. 10 | 11 | For example, in an integration test, this could be used as such: 12 | 13 | ```mermaid 14 | sequenceDiagram 15 | autonumber 16 | actor Test 17 | participant EventScout 18 | participant EventBridge 19 | Test->>EventScout: Start event trail with this pattern 20 | Test-->>EventBridge: Generate an event matching the pattern 21 | EventBridge-->>EventScout: Receive the monitored event 22 | EventScout->>EventScout: Store the monitored event in the trail 23 | Test->>EventScout: Query the events in the trail 24 | Test->>EventScout: Close the trail 25 | ``` 26 | 27 | ## Features 28 | 29 | - **Monitor your events easily**: EventScout allows you to create event trails with dynamic filter patterns on EventBridge 30 | - **Scale your integration tests**: EventScout can create unlimited parallel event trails, event with overlapping patterns, so ou can rule all your integration tests in parallel 31 | - **Pay only for what you use**: EventScout implements a fully Serverless architecture 32 | 33 | ## Installation 34 | 35 | In order to properly work, EventScout needs to: 36 | 37 | - deploy the necessary resources with [@event-scout/construct](./packages/construct/README.md) 38 | - use event trails in your tests with [@event-scout/client](./packages/client/README.md) 39 | - monitor your events in real-time with [event-scout](./packages/event-scout/README.md) CLI 40 | 41 | And you're all set! The power of EventScout is yours! 42 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /commonConfiguration/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "inputs": [ 4 | { 5 | "id": "functionName", 6 | "description": "Enter the name of the function to test", 7 | "default": "health", 8 | "type": "promptString" 9 | } 10 | ], 11 | "configurations": [ 12 | { 13 | "name": "Test CDK", 14 | "type": "node", 15 | "request": "launch", 16 | "cwd": "${workspaceFolder}", 17 | "runtimeExecutable": "pnpm", 18 | "args": [ 19 | "test-cdk", 20 | ], 21 | "sourceMaps": true, 22 | "smartStep": true, 23 | "protocol": "inspector", 24 | "autoAttachChildProcesses": true, 25 | "console": "integratedTerminal", 26 | "outputCapture": "console" 27 | }, 28 | { 29 | "name": "Debug a lambda function λ", 30 | "type": "node", 31 | "request": "launch", 32 | "cwd": "${workspaceFolder}", 33 | "runtimeExecutable": "pnpm", 34 | "args": [ 35 | "serverless", 36 | "invoke", 37 | "local", 38 | "-f", 39 | "${input:functionName}", 40 | "--path", 41 | "functions/${input:functionName}/handler.mock.json" 42 | ], 43 | "sourceMaps": true, 44 | "smartStep": true, 45 | "outFiles": [ 46 | "**/.esbuild/**/*.js" 47 | ], 48 | "protocol": "inspector", 49 | "autoAttachChildProcesses": true, 50 | "console": "integratedTerminal", 51 | "outputCapture": "console" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /commonConfiguration/dependency-cruiser.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('dependency-cruiser').IConfiguration} */ 2 | module.exports = ({ pathNot, path } = { pathNot: [], path: [] }) => ({ 3 | forbidden: [ 4 | { 5 | name: 'no-circular', 6 | severity: 'error', 7 | comment: 8 | 'This dependency is part of a circular relationship. You might want to revise ' + 9 | 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', 10 | from: { pathNot, path }, 11 | to: { 12 | circular: true, 13 | }, 14 | }, 15 | ], 16 | options: { 17 | doNotFollow: { 18 | path: 'node_modules', 19 | dependencyTypes: [ 20 | 'npm', 21 | 'npm-dev', 22 | 'npm-optional', 23 | 'npm-peer', 24 | 'npm-bundled', 25 | 'npm-no-pkg', 26 | ], 27 | }, 28 | 29 | moduleSystems: ['amd', 'cjs', 'es6', 'tsd'], 30 | 31 | tsPreCompilationDeps: true, 32 | 33 | tsConfig: { 34 | fileName: 'tsconfig.json', 35 | }, 36 | 37 | enhancedResolveOptions: { 38 | exportsFields: ['exports'], 39 | 40 | conditionNames: ['import', 'require', 'node', 'default'], 41 | }, 42 | reporterOptions: { 43 | dot: { 44 | collapsePattern: 'node_modules/[^/]+', 45 | }, 46 | archi: { 47 | collapsePattern: 48 | '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/[^/]+', 49 | }, 50 | }, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /contracts/construct-contracts/.dependency-cruiser.cjs: -------------------------------------------------------------------------------- 1 | const commonDependencyCruiserConfig = require('../../commonConfiguration/dependency-cruiser.config'); 2 | 3 | const path = ['src']; 4 | const pathNot = ['dist']; 5 | 6 | module.exports = commonDependencyCruiserConfig({ path, pathNot }); 7 | -------------------------------------------------------------------------------- /contracts/construct-contracts/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /contracts/construct-contracts/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: ['./tsconfig.json'], 4 | tsconfigRootDir: __dirname, 5 | }, 6 | settings: { 7 | 'import/resolver': { 8 | typescript: { 9 | project: __dirname, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /contracts/construct-contracts/.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.lintstagedrc.js'); 2 | module.exports = baseConfig; 3 | -------------------------------------------------------------------------------- /contracts/construct-contracts/.vscode: -------------------------------------------------------------------------------- 1 | ../../commonConfiguration/.vscode -------------------------------------------------------------------------------- /contracts/construct-contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-scout/construct-contracts", 3 | "description": "EventScout contracts: safe interactions between construct and client", 4 | "version": "0.8.0", 5 | "author": "fargito", 6 | "license": "MIT", 7 | "homepage": "https://github.com/fargito/event-scout", 8 | "bugs": "https://github.com/fargito/event-scout/issues", 9 | "repository": "fargito/event-scout.git", 10 | "keywords": [ 11 | "CDK", 12 | "CDK Constructs", 13 | "EventBridge", 14 | "Serverless" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "sideEffects": false, 20 | "files": [ 21 | "dist" 22 | ], 23 | "type": "module", 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/types/index.d.ts", 27 | "scripts": { 28 | "lint-fix": "pnpm linter-base-config --fix", 29 | "lint-fix-all": "pnpm lint-fix .", 30 | "linter-base-config": "eslint --ext=js,ts", 31 | "package": "pnpm package-transpile && pnpm package-types && pnpm package-types-aliases", 32 | "package-transpile": "tsup", 33 | "package-types": "tsc -p tsconfig.build.json", 34 | "package-types-aliases": "tsc-alias -p tsconfig.build.json", 35 | "test": "pnpm test-linter && pnpm test-type && pnpm test-unit && pnpm test-circular", 36 | "test-circular": "pnpm depcruise --config -- .", 37 | "test-linter": "pnpm linter-base-config .", 38 | "test-type": "tsc --noEmit --emitDeclarationOnly false", 39 | "test-unit": "vitest run --coverage --passWithNoTests" 40 | }, 41 | "dependencies": { 42 | "@swarmion/serverless-contracts": "0.35.0", 43 | "json-schema-to-ts": "3.1.1" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "22.15.29", 47 | "@vitest/coverage-v8": "3.2.2", 48 | "concurrently": "9.1.2", 49 | "dependency-cruiser": "16.10.2", 50 | "eslint": "^8.56.0", 51 | "prettier": "3.5.3", 52 | "tsc-alias": "1.8.16", 53 | "tsup": "8.5.0", 54 | "typescript": "5.8.3", 55 | "vite": "6.3.5", 56 | "vite-tsconfig-paths": "5.1.4", 57 | "vitest": "3.2.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/construct-contracts/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "construct-contracts", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "implicitDependencies": [] 6 | } 7 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/entities/eventPattern.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from 'json-schema-to-ts'; 2 | 3 | export const eventPatternSchema = { 4 | type: 'object', 5 | properties: { 6 | version: { type: 'array', items: { type: 'string' } }, 7 | id: { type: 'array', items: { type: 'string' } }, 8 | 'detail-type': { type: 'array', items: { type: 'string' } }, 9 | source: { type: 'array', items: { type: 'string' } }, 10 | account: { type: 'array', items: { type: 'string' } }, 11 | time: { type: 'array', items: { type: 'string' } }, 12 | region: { type: 'array', items: { type: 'string' } }, 13 | resources: { type: 'array', items: { type: 'string' } }, 14 | detail: { type: 'object' }, 15 | }, 16 | required: [], 17 | additionalProperties: false, 18 | } as const; 19 | 20 | export type EventPattern = FromSchema; 21 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './eventPattern'; 2 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './requests'; 2 | export * from './entities'; 3 | export * from './websocket'; 4 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/requests/index.ts: -------------------------------------------------------------------------------- 1 | export * from './listEvents'; 2 | export * from './startEventsTrail'; 3 | export * from './stopEventsTrail'; 4 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/requests/listEvents.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiGatewayContract, 3 | HttpStatusCodes, 4 | } from '@swarmion/serverless-contracts'; 5 | 6 | const pathParametersSchema = { 7 | type: 'object', 8 | properties: { 9 | trailId: { type: 'string' }, 10 | }, 11 | required: ['trailId'], 12 | additionalProperties: false, 13 | } as const; 14 | 15 | export const listEventsContract = new ApiGatewayContract({ 16 | id: 'tests-listEvents', 17 | method: 'GET', 18 | path: '/trail/{trailId}', 19 | integrationType: 'httpApi', 20 | authorizerType: 'aws_iam', 21 | pathParametersSchema, 22 | queryStringParametersSchema: undefined, 23 | headersSchema: undefined, 24 | bodySchema: undefined, 25 | outputSchemas: { 26 | [HttpStatusCodes.OK]: { 27 | type: 'array', 28 | } as const, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/requests/startEventsTrail.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiGatewayContract, 3 | HttpStatusCodes, 4 | } from '@swarmion/serverless-contracts'; 5 | 6 | import { eventPatternSchema } from 'entities/eventPattern'; 7 | 8 | const bodySchema = { 9 | type: 'object', 10 | properties: { 11 | eventPattern: eventPatternSchema, 12 | }, 13 | required: ['eventPattern'], 14 | additionalProperties: false, 15 | } as const; 16 | 17 | const outputSchema = { 18 | type: 'object', 19 | properties: { trailId: { type: 'string' } }, 20 | required: ['trailId'], 21 | additionalProperties: false, 22 | } as const; 23 | 24 | export const startEventsTrailContract = new ApiGatewayContract({ 25 | id: 'tests-startEventsTrail', 26 | method: 'POST', 27 | path: '/start-events-trail', 28 | integrationType: 'httpApi', 29 | authorizerType: 'aws_iam', 30 | pathParametersSchema: undefined, 31 | queryStringParametersSchema: undefined, 32 | headersSchema: undefined, 33 | bodySchema, 34 | outputSchemas: { [HttpStatusCodes.OK]: outputSchema }, 35 | }); 36 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/requests/stopEventsTrail.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiGatewayContract, 3 | HttpStatusCodes, 4 | } from '@swarmion/serverless-contracts'; 5 | 6 | const bodySchema = { 7 | type: 'object', 8 | properties: { 9 | trailId: { type: 'string' }, 10 | }, 11 | required: ['trailId'], 12 | additionalProperties: false, 13 | } as const; 14 | 15 | const outputSchema = { 16 | type: 'object', 17 | properties: { trailId: { type: 'string' } }, 18 | required: ['trailId'], 19 | additionalProperties: false, 20 | } as const; 21 | 22 | export const stopEventsTrailContract = new ApiGatewayContract({ 23 | id: 'tests-stopEventsTrail', 24 | method: 'POST', 25 | path: '/stop-events-trail', 26 | integrationType: 'httpApi', 27 | authorizerType: 'aws_iam', 28 | pathParametersSchema: undefined, 29 | queryStringParametersSchema: undefined, 30 | headersSchema: undefined, 31 | bodySchema, 32 | outputSchemas: { [HttpStatusCodes.OK]: outputSchema }, 33 | }); 34 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/websocket/index.ts: -------------------------------------------------------------------------------- 1 | export * from './startTrail'; 2 | -------------------------------------------------------------------------------- /contracts/construct-contracts/src/websocket/startTrail.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from 'json-schema-to-ts'; 2 | 3 | import { eventPatternSchema } from 'entities/eventPattern'; 4 | 5 | export const startWebsocketEventTrailBodySchema = { 6 | type: 'object', 7 | properties: { 8 | eventPattern: eventPatternSchema, 9 | }, 10 | required: ['eventPattern'], 11 | } as const; 12 | 13 | export type StartWebsocketEventTrailBody = FromSchema< 14 | typeof startWebsocketEventTrailBodySchema 15 | >; 16 | -------------------------------------------------------------------------------- /contracts/construct-contracts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDir": "src", 6 | "outDir": "./dist/types" 7 | }, 8 | "include": ["./**/*.ts"], 9 | "exclude": ["./vite*", "./**/*.test.ts", "./dist", "./tsup.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /contracts/construct-contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src" 5 | }, 6 | "include": ["./**/*.ts"], 7 | "exclude": ["./dist"] 8 | } 9 | -------------------------------------------------------------------------------- /contracts/construct-contracts/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | silent: true, 7 | format: ['cjs', 'esm'], 8 | outDir: 'dist', 9 | }); 10 | -------------------------------------------------------------------------------- /contracts/construct-contracts/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /event-scout.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.fixAll.stylelint": "explicit", 8 | }, 9 | "stylelint.validate": ["typescript", "typescriptreact"], 10 | "stylelint.customSyntax": "@stylelint/postcss-css-in-js", 11 | "search.exclude": { 12 | "**/coverage": true, 13 | "**/node_modules": true, 14 | "**/.serverless": true, 15 | "**/build": true, 16 | "**/.esbuild": true, 17 | "**/bundles": true, 18 | "**/dist": true, 19 | "**/tsconfig.tsbuildinfo": true, 20 | "**/cdk.out": true, 21 | "pnpm-lock.yaml": true, 22 | }, 23 | "[dotenv][ignore][properties][shellscript]": { 24 | "editor.defaultFormatter": "foxundermoon.shell-format", 25 | }, 26 | "[html][javascript][json]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode", 28 | }, 29 | "typescript.tsdk": "node_modules/typescript/lib", 30 | "vitest.commandLine": "pnpm vitest", 31 | "vitest.disabledWorkspaceFolders": ["event-scout root"], 32 | "cSpell.words": ["esbuild", "eventbridge", "swarmion", "tsup"], 33 | }, 34 | "extensions": { 35 | "recommendations": [ 36 | "esbenp.prettier-vscode", 37 | "dbaeumer.vscode-eslint", 38 | "editorconfig.editorconfig", 39 | "foxundermoon.shell-format", 40 | "ZixuanChen.vitest-explorer", 41 | "streetsidesoftware.code-spell-checker", 42 | "streetsidesoftware.code-spell-checker-french", 43 | "stylelint.vscode-stylelint", 44 | "styled-components.vscode-styled-components", 45 | "SebastianBille.iam-legend", 46 | ], 47 | }, 48 | "folders": [ 49 | { 50 | "path": ".", 51 | "name": "event-scout root", 52 | }, 53 | { 54 | "path": "packages/construct", 55 | "name": "construct [library]", 56 | }, 57 | { 58 | "path": "packages/lambda-assets", 59 | "name": "lambda assets [library]", 60 | }, 61 | { 62 | "path": "contracts/construct-contracts", 63 | "name": "construct contracts [library]", 64 | }, 65 | { 66 | "path": "packages/client", 67 | "name": "client [library]", 68 | }, 69 | { 70 | "path": "packages/event-scout", 71 | "name": "event scout [library]", 72 | }, 73 | { 74 | "path": "services/validation", 75 | "name": "validation [service]", 76 | }, 77 | ], 78 | } 79 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "pnpm", 3 | "version": "0.8.0" 4 | } 5 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "event-scout", 3 | "tasksRunnerOptions": { 4 | "default": { 5 | "runner": "nx/tasks-runners/default", 6 | "options": { 7 | "cacheableOperations": [ 8 | "build", 9 | "package", 10 | "test", 11 | "test-circular", 12 | "test-linter", 13 | "test-stylelint", 14 | "test-type", 15 | "test-unit" 16 | ], 17 | "cacheDirectory": "nx-cache" 18 | } 19 | } 20 | }, 21 | "affected": { 22 | "defaultBase": "main" 23 | }, 24 | "namedInputs": { 25 | "default": ["{projectRoot}/**/*"], 26 | "production": ["!{projectRoot}/**/*.test.tsx?"] 27 | }, 28 | "targetDefaults": { 29 | "build": { 30 | "inputs": ["production", "^production"], 31 | "dependsOn": ["^build", "^package"] 32 | }, 33 | "package": { 34 | "inputs": ["production", "^production"], 35 | "dependsOn": ["^package"], 36 | "outputs": ["{projectRoot}/dist"] 37 | }, 38 | "deploy": { 39 | "inputs": ["production", "^production"], 40 | "dependsOn": ["^package", "^deploy", "^build", "bootstrap"] 41 | }, 42 | "deploy-staging": { 43 | "inputs": ["production", "^production"], 44 | "dependsOn": ["^package", "^build", "bootstrap-staging"] 45 | }, 46 | "deploy-production": { 47 | "inputs": ["production", "^production"], 48 | "dependsOn": ["^package", "^build", "bootstrap-production"] 49 | }, 50 | "bootstrap": { 51 | "inputs": ["production", "^production"], 52 | "dependsOn": ["^package", "build", "^build"] 53 | }, 54 | "bootstrap-staging": { 55 | "inputs": ["production", "^production"], 56 | "dependsOn": ["^package", "build", "^build"] 57 | }, 58 | "bootstrap-production": { 59 | "inputs": ["production", "^production"], 60 | "dependsOn": ["^package", "build", "^build"] 61 | }, 62 | "test": { 63 | "inputs": ["default", "^production"], 64 | "dependsOn": ["^package"] 65 | }, 66 | "test-linter": { 67 | "inputs": ["default", "^production"], 68 | "dependsOn": ["^package"] 69 | }, 70 | "test-unit": { 71 | "inputs": ["default", "^production"], 72 | "dependsOn": ["^package"] 73 | }, 74 | "test-type": { 75 | "inputs": ["default", "^production"], 76 | "dependsOn": ["^package"] 77 | }, 78 | "test-circular": { 79 | "inputs": ["default", "^production"], 80 | "dependsOn": ["^package"] 81 | }, 82 | "test-cdk": { 83 | "inputs": ["production", "^production"], 84 | "dependsOn": ["^package", "build", "^build"] 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-scout/root", 3 | "private": true, 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "nx run-many --target=build --all --parallel=4", 8 | "deploy": "nx run-many --target=deploy --all --parallel=4", 9 | "deploy-affected": "nx affected --target=deploy", 10 | "generate-library": "nx generate @swarmion/nx-plugin:library", 11 | "generate-service": "nx generate @swarmion/nx-plugin:service", 12 | "graph": "nx dep-graph", 13 | "info": "nx run-many --target=sls-info --all --parallel=4", 14 | "lint-fix": "eslint --ext=js,ts --fix", 15 | "lint-fix-all": "nx run-many --target=lint-fix-all --all --parallel=4", 16 | "package": "nx run-many --target=package --all --parallel=4", 17 | "postinstall": "husky && syncpack format", 18 | "release": "lerna publish --force-publish", 19 | "test": "nx run-many --target=test --all --parallel=4", 20 | "test-affected": "nx affected --target=test", 21 | "test-circular": "nx run-many --target=test-circular --all --parallel=4", 22 | "test-integration": "nx run-many --target=test-integration --all --parallel=4", 23 | "test-linter": "nx run-many --target=test-linter --all --parallel=4", 24 | "test-type": "nx run-many --target=test-type --all --parallel=4", 25 | "test-unit": "nx run-many --target=test-unit --all --parallel=4" 26 | }, 27 | "devDependencies": { 28 | "@commitlint/cli": "19.8.1", 29 | "@commitlint/config-conventional": "19.8.1", 30 | "@nrwl/workspace": "19.8.14", 31 | "@swarmion/nx-plugin": "0.35.0", 32 | "@typescript-eslint/eslint-plugin": "^7.0.0", 33 | "@typescript-eslint/parser": "^7.0.0", 34 | "changelogithub": "13.15.0", 35 | "dependency-cruiser": "16.10.2", 36 | "eslint": "^8.56.0", 37 | "eslint-config-prettier": "10.1.5", 38 | "eslint-import-resolver-typescript": "4.4.2", 39 | "eslint-plugin-import": "2.31.0", 40 | "eslint-plugin-prefer-arrow": "1.2.3", 41 | "eslint-plugin-prettier": "5.4.1", 42 | "husky": "9.1.7", 43 | "lerna": "8.2.2", 44 | "lint-staged": "16.1.0", 45 | "nx": "21.1.2", 46 | "prettier": "3.5.3", 47 | "syncpack": "13.0.4", 48 | "typescript": "5.8.3" 49 | }, 50 | "engines": { 51 | "node": "^22.0.0" 52 | }, 53 | "packageManager": "pnpm@10.11.1" 54 | } 55 | -------------------------------------------------------------------------------- /packages/client/.dependency-cruiser.cjs: -------------------------------------------------------------------------------- 1 | const commonDependencyCruiserConfig = require('../../commonConfiguration/dependency-cruiser.config'); 2 | 3 | const path = ['src']; 4 | const pathNot = ['dist']; 5 | 6 | module.exports = commonDependencyCruiserConfig({ path, pathNot }); 7 | -------------------------------------------------------------------------------- /packages/client/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: ['./tsconfig.json'], 4 | tsconfigRootDir: __dirname, 5 | }, 6 | settings: { 7 | 'import/resolver': { 8 | typescript: { 9 | project: __dirname, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/client/.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.lintstagedrc.js'); 2 | module.exports = baseConfig; 3 | -------------------------------------------------------------------------------- /packages/client/.vscode: -------------------------------------------------------------------------------- 1 | ../../commonConfiguration/.vscode -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # @event-scout/client 2 | 3 | Create and query event trails using EventScout. 4 | 5 | This repository is part of [EventScout](https://github.com/fargito/event-scout). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pnpm add -D @event-scout/client 11 | ``` 12 | 13 | or if using yarn 14 | 15 | ``` 16 | yarn add --dev @event-scout/client 17 | ``` 18 | 19 | or if using npm 20 | 21 | ``` 22 | npm install --save-dev @event-scout/client 23 | ``` 24 | 25 | ## Use EventScout in your tests 26 | 27 | ### Create a client 28 | 29 | In order to use the EventScout client, you will need: 30 | 31 | - the `endpoint` exported by [@event-scout/construct](https://github.com/fargito/event-scout/main/packages/construct/README.md) 32 | - the AWS `region` in which you run your tests 33 | - valid AWS `credentials`. Check out [the AWS SDK v3 docs on how to build credentials](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers.html). 34 | 35 | Instantiate the client: 36 | 37 | ```ts 38 | import { EventScoutClient } from '@event-scout/client'; 39 | 40 | const eventScoutClient = new EventScoutClient({ 41 | credentials, 42 | region, 43 | endpoint: eventScoutEndpoint, 44 | }); 45 | ``` 46 | 47 | ### Start an events trail 48 | 49 | A trail is a list of events with a certain patterns, that you can dynamically create. 50 | 51 | At the beginning of the test, start a trail with a custom pattern: 52 | 53 | ```ts 54 | beforeAll( 55 | async () => { 56 | await eventScoutClient.start({ 57 | eventPattern: { 58 | source: ['my-pattern'], 59 | 'detail-type': ['MY_DETAIL_TYPE'], 60 | }, 61 | }); 62 | }, 63 | 30 * 1000, // 30s timeout 64 | ); 65 | ``` 66 | 67 | You can put any valid EventBridge pattern here, including content filtering. Check [the AWS docs](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html) for more details. 68 | 69 | ⚠ In order for the resources to be properly activated, the EventScout client will wait for 20 seconds before returning. We advise to set a **30 seconds timeout** on the `beforeAll` method. 70 | 71 | ### Query the trail messages 72 | 73 | In your tests, simply call the `.query()` method to retrieve events in the trail. 74 | 75 | ```ts 76 | const messages = await eventScoutClient.query(); 77 | ``` 78 | 79 | This will return all the events in the trail! You are then free to make assertions on them. 80 | 81 | ### Cleanup 82 | 83 | At the end of the test, call the `.stop()` method in `afterAll`: 84 | 85 | ```ts 86 | afterAll(async () => { 87 | await eventScoutClient.stop(); 88 | }); 89 | ``` 90 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-scout/client", 3 | "description": "EventScout client", 4 | "version": "0.8.0", 5 | "author": "fargito", 6 | "license": "MIT", 7 | "homepage": "https://github.com/fargito/event-scout", 8 | "bugs": "https://github.com/fargito/event-scout/issues", 9 | "repository": "fargito/event-scout.git", 10 | "keywords": [ 11 | "CDK", 12 | "CDK Constructs", 13 | "EventBridge", 14 | "Serverless" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "sideEffects": false, 20 | "files": [ 21 | "dist" 22 | ], 23 | "type": "module", 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/types/index.d.ts", 27 | "scripts": { 28 | "lint-fix": "pnpm linter-base-config --fix", 29 | "lint-fix-all": "pnpm lint-fix .", 30 | "linter-base-config": "eslint --ext=js,ts", 31 | "package": "pnpm package-transpile && pnpm package-types && pnpm package-types-aliases", 32 | "package-transpile": "tsup", 33 | "package-types": "tsc -p tsconfig.build.json", 34 | "package-types-aliases": "tsc-alias -p tsconfig.build.json", 35 | "test": "pnpm test-linter && pnpm test-type && pnpm test-unit && pnpm test-circular", 36 | "test-circular": "pnpm depcruise --config -- .", 37 | "test-linter": "pnpm linter-base-config .", 38 | "test-type": "tsc --noEmit --emitDeclarationOnly false", 39 | "test-unit": "vitest run --coverage --passWithNoTests" 40 | }, 41 | "dependencies": { 42 | "@aws-crypto/sha256-js": "5.2.0", 43 | "@aws-sdk/types": "3.821.0", 44 | "@event-scout/construct-contracts": "workspace:^0.8.0", 45 | "@smithy/signature-v4": "5.1.2", 46 | "@swarmion/serverless-contracts": "0.35.0", 47 | "axios": "1.9.0" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "22.15.29", 51 | "@vitest/coverage-v8": "3.2.2", 52 | "concurrently": "9.1.2", 53 | "dependency-cruiser": "16.10.2", 54 | "eslint": "^8.56.0", 55 | "prettier": "3.5.3", 56 | "tsc-alias": "1.8.16", 57 | "tsup": "8.5.0", 58 | "typescript": "5.8.3", 59 | "vite": "6.3.5", 60 | "vite-tsconfig-paths": "5.1.4", 61 | "vitest": "3.2.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/client/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "implicitDependencies": [] 6 | } 7 | -------------------------------------------------------------------------------- /packages/client/src/EventScoutClient.ts: -------------------------------------------------------------------------------- 1 | import { Sha256 } from '@aws-crypto/sha256-js'; 2 | import type { AwsCredentialIdentityProvider } from '@aws-sdk/types'; 3 | import { SignatureV4 } from '@smithy/signature-v4'; 4 | import { getRequestParameters } from '@swarmion/serverless-contracts'; 5 | import axios, { type AxiosRequestConfig } from 'axios'; 6 | 7 | import { 8 | type EventPattern, 9 | listEventsContract, 10 | startEventsTrailContract, 11 | stopEventsTrailContract, 12 | } from '@event-scout/construct-contracts'; 13 | 14 | export class EventScoutClient { 15 | private endpoint: string; 16 | private trailId?: string; 17 | private signatureV4: SignatureV4; 18 | 19 | constructor({ 20 | credentials, 21 | endpoint, 22 | region, 23 | }: { 24 | credentials: AwsCredentialIdentityProvider; 25 | endpoint: string; 26 | region: string; 27 | }) { 28 | this.endpoint = endpoint; 29 | 30 | this.signatureV4 = new SignatureV4({ 31 | service: 'execute-api', 32 | region, 33 | credentials, 34 | sha256: Sha256, 35 | }); 36 | } 37 | 38 | /** 39 | * Start an events trail with a specific pattern. 40 | * 41 | * @param eventPattern a valid eventBridge pattern. See https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html 42 | * 43 | * ⚠ this function will last for more than 20 seconds to ensure created resources are running. 44 | * We recommend setting a timeout value of 30 seconds in the calling method. 45 | */ 46 | async start({ 47 | eventPattern = {}, 48 | }: { 49 | eventPattern?: EventPattern; 50 | }): Promise { 51 | if (this.trailId !== undefined) { 52 | throw new Error('Only one trail can be started on a single client'); 53 | } 54 | 55 | const url = this.endpoint + 'start-events-trail'; 56 | 57 | const body = { eventPattern }; 58 | 59 | const typedRequest = getRequestParameters(startEventsTrailContract, { 60 | body, 61 | }); 62 | 63 | const config = await this.getSignedAxiosConfig(url, typedRequest); 64 | 65 | const { 66 | data: { trailId }, 67 | } = await axios<{ 68 | trailId: string; 69 | }>(config); 70 | 71 | this.trailId = trailId; 72 | 73 | // rule can take a few seconds to to enabled, waiting for 20 seconds 74 | await new Promise(r => setTimeout(r, 20 * 1000)); 75 | } 76 | 77 | /** 78 | * Stop an events trail. 79 | * 80 | * This will immediately stop new events from being recorded in the trail. 81 | * However, this will not immediately remove all events from the trail, since they have a time to live of 15 minutes 82 | */ 83 | async stop(): Promise { 84 | if (this.trailId === undefined) { 85 | throw new Error('No trail found, did you forget to run .start()?'); 86 | } 87 | 88 | const url = this.endpoint + 'stop-events-trail'; 89 | const body = { trailId: this.trailId }; 90 | 91 | const typedRequest = getRequestParameters(stopEventsTrailContract, { 92 | body, 93 | }); 94 | 95 | const config = await this.getSignedAxiosConfig(url, typedRequest); 96 | 97 | await axios<{ 98 | trailId: string; 99 | }>(config); 100 | } 101 | 102 | /** 103 | * Query the events in the trail. 104 | * 105 | * @returns events in trail. They have an `unknown` type on purpose, you have to make your own assertions. 106 | */ 107 | async query(): Promise { 108 | if (this.trailId === undefined) { 109 | throw new Error('No trail found, did you forget to run .start()?'); 110 | } 111 | 112 | const trailId = this.trailId; 113 | 114 | const url = `${this.endpoint}trail/${trailId}`; 115 | 116 | const typedRequest = getRequestParameters(listEventsContract, { 117 | pathParameters: { trailId }, 118 | }); 119 | 120 | const config = await this.getSignedAxiosConfig(url, typedRequest); 121 | 122 | const { data: events } = await axios(config); 123 | 124 | return events; 125 | } 126 | 127 | /** 128 | * 129 | * @param url 130 | * @param request 131 | * @returns a signed axios config, ready to be called with `axios(config)` 132 | */ 133 | private async getSignedAxiosConfig( 134 | url: string, 135 | request: { path: string; method: string; body?: unknown }, 136 | ): Promise { 137 | const apiUrl = new URL(url); 138 | 139 | if (request.body === undefined) { 140 | const signedRequest = await this.signatureV4.sign({ 141 | method: request.method, 142 | hostname: apiUrl.host, 143 | path: apiUrl.pathname, 144 | protocol: apiUrl.protocol, 145 | headers: { 146 | 'Content-Type': 'application/json', 147 | host: apiUrl.hostname, // compulsory for signature 148 | }, 149 | }); 150 | 151 | return { 152 | ...signedRequest, 153 | url, 154 | }; 155 | } else { 156 | const body = JSON.stringify(request.body); 157 | 158 | const signedRequest = await this.signatureV4.sign({ 159 | method: request.method, 160 | hostname: apiUrl.host, 161 | path: apiUrl.pathname, 162 | protocol: apiUrl.protocol, 163 | body, 164 | headers: { 165 | 'Content-Type': 'application/json', 166 | host: apiUrl.hostname, // compulsory for signature 167 | }, 168 | }); 169 | 170 | return { 171 | ...signedRequest, 172 | url, 173 | data: body, 174 | }; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EventScoutClient'; 2 | -------------------------------------------------------------------------------- /packages/client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDir": "src", 6 | "outDir": "./dist/types" 7 | }, 8 | "include": ["./**/*.ts"], 9 | "exclude": ["./vite*", "./**/*.test.ts", "./dist", "./tsup.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src" 5 | }, 6 | "references": [ 7 | { 8 | "path": "../../contracts/construct-contracts/tsconfig.build.json" 9 | } 10 | ], 11 | "include": ["./**/*.ts"], 12 | "exclude": ["./dist"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | silent: true, 7 | format: ['cjs', 'esm'], 8 | outDir: 'dist', 9 | }); 10 | -------------------------------------------------------------------------------- /packages/client/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/construct/.dependency-cruiser.cjs: -------------------------------------------------------------------------------- 1 | const commonDependencyCruiserConfig = require('../../commonConfiguration/dependency-cruiser.config'); 2 | 3 | const path = ['src']; 4 | const pathNot = ['dist']; 5 | 6 | module.exports = commonDependencyCruiserConfig({ path, pathNot }); 7 | -------------------------------------------------------------------------------- /packages/construct/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/construct/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: ['./tsconfig.json'], 4 | tsconfigRootDir: __dirname, 5 | }, 6 | settings: { 7 | 'import/resolver': { 8 | typescript: { 9 | project: __dirname, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/construct/.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.lintstagedrc.js'); 2 | module.exports = baseConfig; 3 | -------------------------------------------------------------------------------- /packages/construct/.vscode: -------------------------------------------------------------------------------- 1 | ../../commonConfiguration/.vscode -------------------------------------------------------------------------------- /packages/construct/README.md: -------------------------------------------------------------------------------- 1 | # @event-scout/construct 2 | 3 | Create resources necessary to use EventScout. 4 | 5 | This repository is part of [EventScout](https://github.com/fargito/event-scout). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pnpm add -D @event-scout/construct 11 | ``` 12 | 13 | or if using yarn 14 | 15 | ``` 16 | yarn add --dev @event-scout/construct 17 | ``` 18 | 19 | or if using npm 20 | 21 | ``` 22 | npm install --save-dev @event-scout/construct 23 | ``` 24 | 25 | ## Deploy the resources 26 | 27 | The resources for EventScout are only available through a CDK construct for the moment. 28 | 29 | Instantiate the CDK construct: 30 | 31 | ```ts 32 | import { EventScout } from '@event-scout/construct'; 33 | import { CfnOutput } from 'aws-cdk-lib'; 34 | import { EventBus } from 'aws-cdk-lib/aws-events'; 35 | 36 | // create the necessary resources 37 | const { httpEndpoint } = new EventScout(this, 'EventScout', { 38 | eventBus: EventBus.fromEventBusName(this, 'EventBus', eventBusName), 39 | }); 40 | 41 | // export the endpoint value for easier use in tests 42 | new CfnOutput(this, 'EventScoutEndpoint', { 43 | value: httpEndpoint, 44 | description: 'EventScout endpoint', 45 | exportName: '', 46 | }); 47 | ``` 48 | 49 | The export here will be useful to retrieve the EventScout endpoint for your tests. 50 | 51 | ⚠ Since the `EventScout` construct provisions Lambda, it must be deployed with the CDK and is not compatible with [@swarmion/serverless-cdk-plugin](https://github.com/swarmion/swarmion/tree/main/packages/serverless-contracts-plugin) 52 | -------------------------------------------------------------------------------- /packages/construct/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-scout/construct", 3 | "description": "EventScout: construct to ease EventBridge monitoring and testing", 4 | "version": "0.8.0", 5 | "author": "fargito", 6 | "license": "MIT", 7 | "homepage": "https://github.com/fargito/event-scout", 8 | "bugs": "https://github.com/fargito/event-scout/issues", 9 | "repository": "fargito/event-scout.git", 10 | "keywords": [ 11 | "CDK", 12 | "CDK Constructs", 13 | "EventBridge", 14 | "Serverless" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "sideEffects": false, 20 | "files": [ 21 | "dist" 22 | ], 23 | "type": "module", 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/types/index.d.ts", 27 | "scripts": { 28 | "lint-fix": "pnpm linter-base-config --fix", 29 | "lint-fix-all": "pnpm lint-fix .", 30 | "linter-base-config": "eslint --ext=js,ts", 31 | "package": "pnpm package-transpile && pnpm package-types && pnpm package-types-aliases", 32 | "package-transpile": "tsup", 33 | "package-types": "tsc -p tsconfig.build.json", 34 | "package-types-aliases": "tsc-alias -p tsconfig.build.json", 35 | "test": "pnpm test-linter && pnpm test-type && pnpm test-unit && pnpm test-circular", 36 | "test-circular": "pnpm depcruise --config -- .", 37 | "test-linter": "pnpm linter-base-config .", 38 | "test-type": "tsc --noEmit --emitDeclarationOnly false", 39 | "test-unit": "vitest run --coverage --passWithNoTests" 40 | }, 41 | "dependencies": { 42 | "@event-scout/lambda-assets": "workspace:^0.8.0", 43 | "aws-cdk-lib": "2.200.1", 44 | "constructs": "10.4.2" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "22.15.29", 48 | "@vitest/coverage-v8": "3.2.2", 49 | "dependency-cruiser": "16.10.2", 50 | "eslint": "^8.56.0", 51 | "prettier": "3.5.3", 52 | "tsc-alias": "1.8.16", 53 | "tsup": "8.5.0", 54 | "typescript": "5.8.3", 55 | "vite": "6.3.5", 56 | "vite-tsconfig-paths": "5.1.4", 57 | "vitest": "3.2.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/construct/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "construct", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "implicitDependencies": [] 6 | } 7 | -------------------------------------------------------------------------------- /packages/construct/src/EventScout.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy } from 'aws-cdk-lib'; 2 | import { 3 | AttributeType, 4 | BillingMode, 5 | StreamViewType, 6 | Table, 7 | } from 'aws-cdk-lib/aws-dynamodb'; 8 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 9 | import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 10 | import { Construct } from 'constructs'; 11 | 12 | import { TrailGarbageCollectorFunction } from './httpApiTrail/functions/trailGarbageCollector'; 13 | import { HttpApiTrail } from './httpApiTrail/httpApiTrail'; 14 | import { WebSocketTrail } from './webSocketTrail/webSocketTrail'; 15 | 16 | type EventScoutProps = { 17 | eventBus: IEventBus; 18 | stage?: string; 19 | }; 20 | 21 | export class EventScout extends Construct { 22 | httpEndpoint: string; 23 | /** 24 | * @deprecated use `httpEndpoint` instead 25 | */ 26 | restEndpoint: string; 27 | webSocketEndpoint: string; 28 | 29 | /** 30 | * The construct to provide all necessary resources for EventScout. 31 | */ 32 | constructor( 33 | scope: Construct, 34 | id: string, 35 | { eventBus, stage = 'dev' }: EventScoutProps, 36 | ) { 37 | super(scope, id); 38 | 39 | // provision the Table 40 | const table = new Table(this, 'Table', { 41 | partitionKey: { name: 'PK', type: AttributeType.STRING }, 42 | sortKey: { name: 'SK', type: AttributeType.STRING }, 43 | billingMode: BillingMode.PAY_PER_REQUEST, 44 | timeToLiveAttribute: '_ttl', 45 | stream: StreamViewType.NEW_AND_OLD_IMAGES, 46 | removalPolicy: RemovalPolicy.DESTROY, 47 | }); 48 | 49 | const logGroup = new LogGroup(this, 'Logs', { 50 | retention: RetentionDays.FIVE_DAYS, 51 | removalPolicy: RemovalPolicy.DESTROY, // do not keep log group if it is no longer included in a deployment 52 | }); 53 | 54 | // Lambda to listen to trail items deletion and delete the eventBridge resources 55 | // therefore it is safe to not call the stop lambda 56 | new TrailGarbageCollectorFunction(this, 'TrailGarbageCollectorFunction', { 57 | table, 58 | eventBus, 59 | logGroup, 60 | }); 61 | 62 | // create all necessary resource 63 | const { httpEndpoint } = new HttpApiTrail(this, 'HttpApiTrail', { 64 | table, 65 | eventBus, 66 | logGroup, 67 | }); 68 | this.httpEndpoint = httpEndpoint; 69 | this.restEndpoint = httpEndpoint; 70 | 71 | const { webSocketEndpoint } = new WebSocketTrail(this, 'WebsocketTrail', { 72 | table, 73 | eventBus, 74 | logGroup, 75 | stage, 76 | }); 77 | this.webSocketEndpoint = webSocketEndpoint; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/construct/src/httpApiTrail/functions/storeEvents.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Fn } from 'aws-cdk-lib'; 2 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 3 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 4 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { 6 | Architecture, 7 | CfnPermission, 8 | Code, 9 | Function as LambdaFunction, 10 | LoggingFormat, 11 | Runtime, 12 | } from 'aws-cdk-lib/aws-lambda'; 13 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 14 | import { Construct } from 'constructs'; 15 | 16 | type Props = { 17 | table: Table; 18 | eventBus: IEventBus; 19 | logGroup: ILogGroup; 20 | }; 21 | 22 | export class StoreEventsFunction extends Construct { 23 | public function: LambdaFunction; 24 | 25 | constructor( 26 | scope: Construct, 27 | id: string, 28 | { table, eventBus, logGroup }: Props, 29 | ) { 30 | super(scope, id); 31 | 32 | this.function = new LambdaFunction(this, 'StoreEvents', { 33 | code: Code.fromAsset( 34 | require.resolve('@event-scout/lambda-assets/storeEvents'), 35 | ), 36 | handler: 'handler.main', 37 | runtime: Runtime.NODEJS_22_X, 38 | architecture: Architecture.ARM_64, 39 | environment: { 40 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 41 | EVENT_SCOUT_TABLE_NAME: table.tableName, 42 | }, 43 | loggingFormat: LoggingFormat.JSON, 44 | logGroup, 45 | initialPolicy: [ 46 | new PolicyStatement({ 47 | effect: Effect.ALLOW, 48 | resources: [table.tableArn], 49 | actions: ['dynamodb:PutItem'], 50 | }), 51 | ], 52 | }); 53 | 54 | // enable **any** rule on our bus to trigger the lambda 55 | new CfnPermission(this, 'Permission', { 56 | action: 'lambda:InvokeFunction', 57 | functionName: this.function.functionName, 58 | principal: 'events.amazonaws.com', 59 | sourceArn: Fn.join(':', [ 60 | 'arn', 61 | Aws.PARTITION, 62 | 'events', 63 | Aws.REGION, 64 | Aws.ACCOUNT_ID, 65 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 66 | ]), 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/construct/src/httpApiTrail/functions/trailGarbageCollector.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Duration, Fn } from 'aws-cdk-lib'; 2 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 3 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 4 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { 6 | Architecture, 7 | Code, 8 | FilterCriteria, 9 | FilterRule, 10 | Function as LambdaFunction, 11 | LoggingFormat, 12 | Runtime, 13 | StartingPosition, 14 | } from 'aws-cdk-lib/aws-lambda'; 15 | import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 16 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 17 | import { Construct } from 'constructs'; 18 | 19 | type Props = { 20 | table: Table; 21 | eventBus: IEventBus; 22 | logGroup: ILogGroup; 23 | }; 24 | 25 | export class TrailGarbageCollectorFunction extends Construct { 26 | public function: LambdaFunction; 27 | 28 | constructor( 29 | scope: Construct, 30 | id: string, 31 | { table, eventBus, logGroup }: Props, 32 | ) { 33 | super(scope, id); 34 | 35 | this.function = new LambdaFunction(this, 'TrailGarbageCollector', { 36 | code: Code.fromAsset( 37 | require.resolve('@event-scout/lambda-assets/trailGarbageCollector'), 38 | ), 39 | handler: 'handler.main', 40 | runtime: Runtime.NODEJS_22_X, 41 | architecture: Architecture.ARM_64, 42 | timeout: Duration.minutes(1), 43 | environment: { 44 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 45 | EVENT_BUS_NAME: eventBus.eventBusName, 46 | }, 47 | loggingFormat: LoggingFormat.JSON, 48 | logGroup, 49 | initialPolicy: [ 50 | new PolicyStatement({ 51 | effect: Effect.ALLOW, 52 | resources: [ 53 | Fn.join(':', [ 54 | 'arn', 55 | Aws.PARTITION, 56 | 'events', 57 | Aws.REGION, 58 | Aws.ACCOUNT_ID, 59 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 60 | ]), 61 | ], 62 | actions: ['events:DeleteRule', 'events:RemoveTargets'], 63 | }), 64 | ], 65 | events: [ 66 | new DynamoEventSource(table, { 67 | startingPosition: StartingPosition.TRIM_HORIZON, 68 | batchSize: 1, 69 | filters: [ 70 | FilterCriteria.filter({ 71 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 72 | eventName: FilterRule.isEqual('REMOVE'), 73 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 74 | dynamodb: { Keys: { SK: { S: FilterRule.isEqual('TRAIL') } } }, 75 | }), 76 | ], 77 | retryAttempts: 3, 78 | }), 79 | ], 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/construct/src/httpApiTrail/httpApiTrail.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Fn } from 'aws-cdk-lib'; 2 | import { HttpApi, HttpMethod } from 'aws-cdk-lib/aws-apigatewayv2'; 3 | import { HttpIamAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; 4 | import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; 5 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 6 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 7 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 8 | import { 9 | Architecture, 10 | Code, 11 | Function as LambdaFunction, 12 | LoggingFormat, 13 | Runtime, 14 | } from 'aws-cdk-lib/aws-lambda'; 15 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 16 | import { Construct } from 'constructs'; 17 | 18 | import { StoreEventsFunction } from './functions/storeEvents'; 19 | 20 | type HttpApiTrailProps = { 21 | table: Table; 22 | eventBus: IEventBus; 23 | logGroup: ILogGroup; 24 | }; 25 | 26 | type LambdaConfig = { 27 | codePath: string; 28 | policy: PolicyStatement[]; 29 | httpPath: string; 30 | httpMethod: HttpMethod; 31 | }; 32 | 33 | export class HttpApiTrail extends Construct { 34 | httpEndpoint: string; 35 | 36 | /** 37 | * all the required resources to implement the HTTP api trail, i.e. that 38 | * can be queried by calling an HTTP endpoint. 39 | * 40 | * This is the recommended way for testing 41 | */ 42 | constructor( 43 | scope: Construct, 44 | id: string, 45 | { table, eventBus, logGroup }: HttpApiTrailProps, 46 | ) { 47 | super(scope, id); 48 | 49 | // Add API 50 | const httpApi = new HttpApi(this, 'HttpApi', { 51 | defaultAuthorizer: new HttpIamAuthorizer(), 52 | }); 53 | this.httpEndpoint = httpApi.url as string; 54 | 55 | // Lambda to store events in DynamoDB 56 | const { function: storeEvents } = new StoreEventsFunction( 57 | this, 58 | 'StoreEvents', 59 | { table, eventBus, logGroup }, 60 | ); 61 | 62 | const syncLambdas: Record = { 63 | StartEventsTrail: { 64 | codePath: 'startEventsTrail', 65 | httpMethod: HttpMethod.POST, 66 | httpPath: '/start-events-trail', 67 | policy: [ 68 | new PolicyStatement({ 69 | effect: Effect.ALLOW, 70 | resources: [table.tableArn], 71 | actions: ['dynamodb:PutItem'], 72 | }), 73 | new PolicyStatement({ 74 | effect: Effect.ALLOW, 75 | resources: [ 76 | Fn.join(':', [ 77 | 'arn', 78 | Aws.PARTITION, 79 | 'events', 80 | Aws.REGION, 81 | Aws.ACCOUNT_ID, 82 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 83 | ]), 84 | ], 85 | actions: ['events:PutRule', 'events:PutTargets'], 86 | }), 87 | ], 88 | }, 89 | StopEventsTrail: { 90 | codePath: 'stopEventsTrail', 91 | httpMethod: HttpMethod.POST, 92 | httpPath: '/stop-events-trail', 93 | policy: [ 94 | new PolicyStatement({ 95 | effect: Effect.ALLOW, 96 | resources: [table.tableArn], 97 | actions: ['dynamodb:DeleteItem'], 98 | }), 99 | new PolicyStatement({ 100 | effect: Effect.ALLOW, 101 | resources: [ 102 | Fn.join(':', [ 103 | 'arn', 104 | Aws.PARTITION, 105 | 'events', 106 | Aws.REGION, 107 | Aws.ACCOUNT_ID, 108 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 109 | ]), 110 | ], 111 | actions: ['events:DeleteRule', 'events:RemoveTargets'], 112 | }), 113 | ], 114 | }, 115 | ListEvents: { 116 | codePath: 'listEvents', 117 | httpMethod: HttpMethod.GET, 118 | httpPath: '/trail/{trailId}', 119 | policy: [ 120 | new PolicyStatement({ 121 | effect: Effect.ALLOW, 122 | resources: [table.tableArn], 123 | actions: ['dynamodb:Query'], 124 | }), 125 | ], 126 | }, 127 | }; 128 | 129 | // HTTP Lambdas config 130 | Object.entries(syncLambdas).map(([lambdaName, lambdaConfig]) => { 131 | // create the lambda 132 | const lambda = new LambdaFunction(this, lambdaName, { 133 | architecture: Architecture.ARM_64, 134 | runtime: Runtime.NODEJS_22_X, 135 | code: Code.fromAsset( 136 | require.resolve( 137 | `@event-scout/lambda-assets/${lambdaConfig.codePath}`, 138 | ), 139 | ), 140 | handler: 'handler.main', 141 | memorySize: 1024, 142 | loggingFormat: LoggingFormat.JSON, 143 | logGroup, 144 | environment: { 145 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 146 | EVENT_SCOUT_TABLE_NAME: table.tableName, 147 | EVENT_BUS_NAME: eventBus.eventBusName, 148 | STORE_EVENTS_LAMBDA_ARN: storeEvents.functionArn, 149 | }, 150 | initialPolicy: lambdaConfig.policy, 151 | }); 152 | 153 | // add it to the http api 154 | httpApi.addRoutes({ 155 | path: lambdaConfig.httpPath, 156 | methods: [lambdaConfig.httpMethod], 157 | integration: new HttpLambdaIntegration( 158 | `${lambdaName}Integration`, 159 | lambda, 160 | ), 161 | }); 162 | }); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /packages/construct/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EventScout'; 2 | -------------------------------------------------------------------------------- /packages/construct/src/webSocketTrail/functions/forwardEvent.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Fn } from 'aws-cdk-lib'; 2 | import { WebSocketApi } from 'aws-cdk-lib/aws-apigatewayv2'; 3 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 4 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { 6 | Architecture, 7 | CfnPermission, 8 | Code, 9 | Function as LambdaFunction, 10 | LoggingFormat, 11 | Runtime, 12 | } from 'aws-cdk-lib/aws-lambda'; 13 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 14 | import { Construct } from 'constructs'; 15 | 16 | type Props = { 17 | eventBus: IEventBus; 18 | logGroup: ILogGroup; 19 | webSocketApi: WebSocketApi; 20 | webSocketEndpoint: string; 21 | }; 22 | 23 | export class ForwardEventFunction extends Construct { 24 | public function: LambdaFunction; 25 | 26 | constructor( 27 | scope: Construct, 28 | id: string, 29 | { eventBus, logGroup, webSocketApi, webSocketEndpoint }: Props, 30 | ) { 31 | super(scope, id); 32 | 33 | this.function = new LambdaFunction(this, 'OnNewWebsocketEvent', { 34 | code: Code.fromAsset( 35 | require.resolve('@event-scout/lambda-assets/forwardEvent'), 36 | ), 37 | handler: 'handler.main', 38 | runtime: Runtime.NODEJS_22_X, 39 | architecture: Architecture.ARM_64, 40 | environment: { 41 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 42 | WEBSOCKET_ENDPOINT: webSocketEndpoint, 43 | }, 44 | loggingFormat: LoggingFormat.JSON, 45 | logGroup, 46 | initialPolicy: [ 47 | new PolicyStatement({ 48 | effect: Effect.ALLOW, 49 | resources: [ 50 | `arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${webSocketApi.apiId}/*`, 51 | ], 52 | actions: ['execute-api:ManageConnections'], 53 | }), 54 | ], 55 | }); 56 | 57 | // enable **any** rule on our bus to trigger the lambda 58 | new CfnPermission(this, 'Permission', { 59 | action: 'lambda:InvokeFunction', 60 | functionName: this.function.functionName, 61 | principal: 'events.amazonaws.com', 62 | sourceArn: Fn.join(':', [ 63 | 'arn', 64 | Aws.PARTITION, 65 | 'events', 66 | Aws.REGION, 67 | Aws.ACCOUNT_ID, 68 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 69 | ]), 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/construct/src/webSocketTrail/functions/onStartTrail.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Duration, Fn } from 'aws-cdk-lib'; 2 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 3 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 4 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { 6 | Architecture, 7 | Code, 8 | Function as LambdaFunction, 9 | LoggingFormat, 10 | Runtime, 11 | } from 'aws-cdk-lib/aws-lambda'; 12 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 13 | import { Construct } from 'constructs'; 14 | 15 | type Props = { 16 | table: Table; 17 | eventBus: IEventBus; 18 | logGroup: ILogGroup; 19 | forwardEvent: LambdaFunction; 20 | }; 21 | 22 | export class OnStartTrailFunction extends Construct { 23 | public function: LambdaFunction; 24 | 25 | constructor( 26 | scope: Construct, 27 | id: string, 28 | { table, eventBus, logGroup, forwardEvent }: Props, 29 | ) { 30 | super(scope, id); 31 | 32 | this.function = new LambdaFunction(this, 'OnStartTrail', { 33 | code: Code.fromAsset( 34 | require.resolve('@event-scout/lambda-assets/onStartTrail'), 35 | ), 36 | handler: 'handler.main', 37 | runtime: Runtime.NODEJS_22_X, 38 | architecture: Architecture.ARM_64, 39 | timeout: Duration.seconds(15), 40 | environment: { 41 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 42 | EVENT_SCOUT_TABLE_NAME: table.tableName, 43 | EVENT_BUS_NAME: eventBus.eventBusName, 44 | FORWARD_EVENT_LAMBDA_ARN: forwardEvent.functionArn, 45 | }, 46 | loggingFormat: LoggingFormat.JSON, 47 | logGroup, 48 | initialPolicy: [ 49 | new PolicyStatement({ 50 | effect: Effect.ALLOW, 51 | resources: [table.tableArn], 52 | actions: ['dynamodb:UpdateItem'], 53 | }), 54 | new PolicyStatement({ 55 | effect: Effect.ALLOW, 56 | resources: [ 57 | Fn.join(':', [ 58 | 'arn', 59 | Aws.PARTITION, 60 | 'events', 61 | Aws.REGION, 62 | Aws.ACCOUNT_ID, 63 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 64 | ]), 65 | ], 66 | actions: ['events:PutRule', 'events:PutTargets'], 67 | }), 68 | ], 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/construct/src/webSocketTrail/functions/onWebSocketConnect.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 3 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 4 | import { 5 | Architecture, 6 | Code, 7 | Function as LambdaFunction, 8 | LoggingFormat, 9 | Runtime, 10 | } from 'aws-cdk-lib/aws-lambda'; 11 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 12 | import { Construct } from 'constructs'; 13 | 14 | type Props = { 15 | table: Table; 16 | logGroup: ILogGroup; 17 | }; 18 | 19 | export class OnConnectFunction extends Construct { 20 | public function: LambdaFunction; 21 | 22 | constructor(scope: Construct, id: string, { table, logGroup }: Props) { 23 | super(scope, id); 24 | 25 | this.function = new LambdaFunction(this, 'OnConnect', { 26 | code: Code.fromAsset( 27 | require.resolve('@event-scout/lambda-assets/onWebSocketConnect'), 28 | ), 29 | handler: 'handler.main', 30 | runtime: Runtime.NODEJS_22_X, 31 | architecture: Architecture.ARM_64, 32 | timeout: Duration.seconds(15), 33 | environment: { 34 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 35 | EVENT_SCOUT_TABLE_NAME: table.tableName, 36 | }, 37 | loggingFormat: LoggingFormat.JSON, 38 | logGroup, 39 | initialPolicy: [ 40 | new PolicyStatement({ 41 | effect: Effect.ALLOW, 42 | resources: [table.tableArn], 43 | actions: ['dynamodb:PutItem'], 44 | }), 45 | ], 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/construct/src/webSocketTrail/functions/onWebSocketDisconnect.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Duration, Fn } from 'aws-cdk-lib'; 2 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 3 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 4 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { 6 | Architecture, 7 | Code, 8 | Function as LambdaFunction, 9 | LoggingFormat, 10 | Runtime, 11 | } from 'aws-cdk-lib/aws-lambda'; 12 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 13 | import { Construct } from 'constructs'; 14 | 15 | type Props = { 16 | table: Table; 17 | logGroup: ILogGroup; 18 | eventBus: IEventBus; 19 | }; 20 | 21 | export class OnDisconnectFunction extends Construct { 22 | public function: LambdaFunction; 23 | 24 | constructor( 25 | scope: Construct, 26 | id: string, 27 | { table, logGroup, eventBus }: Props, 28 | ) { 29 | super(scope, id); 30 | 31 | this.function = new LambdaFunction(this, 'OnDisconnect', { 32 | code: Code.fromAsset( 33 | require.resolve('@event-scout/lambda-assets/onWebSocketDisconnect'), 34 | ), 35 | handler: 'handler.main', 36 | runtime: Runtime.NODEJS_22_X, 37 | architecture: Architecture.ARM_64, 38 | timeout: Duration.seconds(15), 39 | environment: { 40 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 41 | EVENT_SCOUT_TABLE_NAME: table.tableName, 42 | EVENT_BUS_NAME: eventBus.eventBusName, 43 | }, 44 | loggingFormat: LoggingFormat.JSON, 45 | logGroup, 46 | initialPolicy: [ 47 | new PolicyStatement({ 48 | effect: Effect.ALLOW, 49 | resources: [table.tableArn], 50 | actions: ['dynamodb:DeleteItem'], 51 | }), 52 | new PolicyStatement({ 53 | effect: Effect.ALLOW, 54 | resources: [ 55 | Fn.join(':', [ 56 | 'arn', 57 | Aws.PARTITION, 58 | 'events', 59 | Aws.REGION, 60 | Aws.ACCOUNT_ID, 61 | Fn.join('/', ['rule', eventBus.eventBusName, '*']), 62 | ]), 63 | ], 64 | actions: ['events:DeleteRule', 'events:RemoveTargets'], 65 | }), 66 | ], 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/construct/src/webSocketTrail/webSocketTrail.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketApi, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; 2 | import { WebSocketIamAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; 3 | import { WebSocketLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; 4 | import { Table } from 'aws-cdk-lib/aws-dynamodb'; 5 | import type { IEventBus } from 'aws-cdk-lib/aws-events'; 6 | import type { ILogGroup } from 'aws-cdk-lib/aws-logs'; 7 | import { Construct } from 'constructs'; 8 | 9 | import { ForwardEventFunction } from './functions/forwardEvent'; 10 | import { OnStartTrailFunction } from './functions/onStartTrail'; 11 | import { OnConnectFunction } from './functions/onWebSocketConnect'; 12 | import { OnDisconnectFunction } from './functions/onWebSocketDisconnect'; 13 | 14 | type WebSocketTrailProps = { 15 | table: Table; 16 | eventBus: IEventBus; 17 | logGroup: ILogGroup; 18 | stage: string; 19 | }; 20 | 21 | export class WebSocketTrail extends Construct { 22 | webSocketEndpoint: string; 23 | /** 24 | * all the required resources to implement the websocket trail 25 | * 26 | * This is the recommended method for developing 27 | */ 28 | constructor( 29 | scope: Construct, 30 | id: string, 31 | { table, eventBus, logGroup, stage }: WebSocketTrailProps, 32 | ) { 33 | super(scope, id); 34 | 35 | const webSocketApi = new WebSocketApi(this, 'WebSocket'); 36 | const webSocketEndpoint = `${webSocketApi.apiEndpoint}/${stage}`; 37 | this.webSocketEndpoint = webSocketEndpoint; 38 | 39 | const { function: forwardEvent } = new ForwardEventFunction( 40 | this, 41 | 'ForwardEvent', 42 | { 43 | eventBus, 44 | logGroup, 45 | webSocketApi, 46 | webSocketEndpoint, 47 | }, 48 | ); 49 | 50 | const { function: onConnect } = new OnConnectFunction(this, 'OnConnect', { 51 | table, 52 | logGroup, 53 | }); 54 | 55 | const { function: onDisconnect } = new OnDisconnectFunction( 56 | this, 57 | 'OnDisconnect', 58 | { 59 | table, 60 | eventBus, 61 | logGroup, 62 | }, 63 | ); 64 | 65 | const { function: onStartTrail } = new OnStartTrailFunction( 66 | this, 67 | 'OnStartTrail', 68 | { table, eventBus, logGroup, forwardEvent }, 69 | ); 70 | 71 | // create routes for API Gateway 72 | webSocketApi.addRoute('$connect', { 73 | integration: new WebSocketLambdaIntegration( 74 | 'ConnectIntegration', 75 | onConnect, 76 | ), 77 | authorizer: new WebSocketIamAuthorizer(), 78 | }); 79 | webSocketApi.addRoute('$disconnect', { 80 | integration: new WebSocketLambdaIntegration( 81 | 'DisconnectIntegration', 82 | onDisconnect, 83 | ), 84 | }); 85 | webSocketApi.addRoute('startTrail', { 86 | integration: new WebSocketLambdaIntegration( 87 | 'StartTrailIntegration', 88 | onStartTrail, 89 | ), 90 | }); 91 | 92 | new WebSocketStage(this, 'Stage', { 93 | webSocketApi, 94 | stageName: stage, 95 | autoDeploy: true, 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/construct/tests/stack.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { EventBus } from 'aws-cdk-lib/aws-events'; 4 | 5 | import { EventScout } from '../src/EventScout'; 6 | 7 | test('Resources are properly created', () => { 8 | const app = new cdk.App(); 9 | const stack = new cdk.Stack(app, 'Stack'); 10 | 11 | const eventBus = new EventBus(stack, 'EventBus'); 12 | 13 | new EventScout(stack, 'EventScout', { 14 | eventBus, 15 | }); 16 | 17 | const template = Template.fromStack(stack); 18 | 19 | template.allResourcesProperties('AWS::Lambda::Function', { 20 | // all functions should have arm64 architecture 21 | Architectures: ['arm64'], 22 | // all functions should have json logging 23 | LoggingConfig: { LogFormat: 'JSON' }, 24 | // all functions should have the same runtime 25 | Runtime: 'nodejs22.x', 26 | }); 27 | 28 | // we should only have one log group 29 | template.resourceCountIs('AWS::Logs::LogGroup', 1); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/construct/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDir": "src", 6 | "outDir": "./dist/types", 7 | "composite": false, 8 | "incremental": false 9 | }, 10 | "files": ["./src/index.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/construct/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src" 5 | }, 6 | "references": [ 7 | { 8 | "path": "../../contracts/construct-contracts/tsconfig.build.json" 9 | } 10 | ], 11 | "include": ["./**/*.ts"], 12 | "exclude": ["./dist"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/construct/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | silent: true, 7 | format: ['cjs', 'esm'], 8 | outDir: 'dist', 9 | }); 10 | -------------------------------------------------------------------------------- /packages/construct/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/event-scout/.dependency-cruiser.js: -------------------------------------------------------------------------------- 1 | const commonDependencyCruiserConfig = require('../../commonConfiguration/dependency-cruiser.config'); 2 | 3 | const path = ['src']; 4 | const pathNot = ['dist']; 5 | 6 | module.exports = commonDependencyCruiserConfig({ path, pathNot }); 7 | -------------------------------------------------------------------------------- /packages/event-scout/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/event-scout/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: ['./tsconfig.json'], 4 | tsconfigRootDir: __dirname, 5 | }, 6 | settings: { 7 | 'import/resolver': { 8 | typescript: { 9 | project: __dirname, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/event-scout/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.lintstagedrc.js'); 2 | module.exports = baseConfig; 3 | -------------------------------------------------------------------------------- /packages/event-scout/.vscode: -------------------------------------------------------------------------------- 1 | ../../commonConfiguration/.vscode -------------------------------------------------------------------------------- /packages/event-scout/README.md: -------------------------------------------------------------------------------- 1 | # Event Scout 2 | 3 | This is the CLI for event-scout. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pnpm add event-scout 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```bash 14 | event-scout --aws-profile='' --aws-region='' --endpoint='' --pattern='{"source": ["toto"]}' 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/event-scout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-scout", 3 | "description": "EventScout CLI", 4 | "version": "0.8.0", 5 | "author": "fargito", 6 | "license": "MIT", 7 | "homepage": "https://github.com/fargito/event-scout", 8 | "bugs": "https://github.com/fargito/event-scout/issues", 9 | "repository": "fargito/event-scout.git", 10 | "keywords": [ 11 | "CDK", 12 | "CDK Constructs", 13 | "EventBridge", 14 | "Serverless" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "sideEffects": false, 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "ncc build ./src/index.ts -o ./dist/ --minify --no-cache --no-source-map-register", 25 | "lint-fix": "pnpm linter-base-config --fix", 26 | "lint-fix-all": "pnpm lint-fix .", 27 | "linter-base-config": "eslint --ext=js,ts", 28 | "test": "pnpm test-linter && pnpm test-type && pnpm test-unit && pnpm test-circular", 29 | "test-circular": "pnpm depcruise --config -- .", 30 | "test-linter": "pnpm linter-base-config .", 31 | "test-type": "tsc --noEmit --emitDeclarationOnly false", 32 | "test-unit": "vitest run --coverage --passWithNoTests" 33 | }, 34 | "bin": "./dist/index.js", 35 | "dependencies": { 36 | "@aws-crypto/sha256-js": "5.2.0", 37 | "@aws-sdk/credential-providers": "3.823.0", 38 | "@aws-sdk/types": "3.821.0", 39 | "@event-scout/construct-contracts": "workspace:^", 40 | "@smithy/signature-v4": "5.1.2", 41 | "ajv": "8.17.1", 42 | "commander": "14.0.0", 43 | "ws": "8.18.2" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "22.15.29", 47 | "@types/ws": "8.18.1", 48 | "@vercel/ncc": "0.38.3", 49 | "@vitest/coverage-v8": "3.2.2", 50 | "concurrently": "9.1.2", 51 | "dependency-cruiser": "16.10.2", 52 | "eslint": "^8.56.0", 53 | "prettier": "3.5.3", 54 | "tsc-alias": "1.8.16", 55 | "typescript": "5.8.3", 56 | "vite": "6.3.5", 57 | "vite-tsconfig-paths": "5.1.4", 58 | "vitest": "3.2.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/event-scout/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-scout", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "implicitDependencies": [] 6 | } 7 | -------------------------------------------------------------------------------- /packages/event-scout/src/buildArguments.ts: -------------------------------------------------------------------------------- 1 | import { fromEnv, fromIni } from '@aws-sdk/credential-providers'; 2 | import type { AwsCredentialIdentityProvider } from '@aws-sdk/types'; 3 | import Ajv from 'ajv'; 4 | import type { OptionValues } from 'commander'; 5 | 6 | import { 7 | type EventPattern, 8 | eventPatternSchema, 9 | } from '@event-scout/construct-contracts'; 10 | 11 | type ParsedArgs = { 12 | webSocketEndpoint: string; 13 | credentials: AwsCredentialIdentityProvider; 14 | region: string; 15 | eventPattern: EventPattern; 16 | }; 17 | 18 | export const buildArguments = (options: OptionValues): ParsedArgs => { 19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 20 | const profileArg: string | undefined = options.awsProfile; 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | const regionArg: string | undefined = options.awsRegion; 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 24 | const endpointArg: string | undefined = options.endpoint; 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 26 | const patternArg: string | undefined = options.pattern; 27 | 28 | const credentials = 29 | profileArg !== undefined ? fromIni({ profile: profileArg }) : fromEnv(); 30 | 31 | const region = regionArg ?? 'eu-west-1'; 32 | 33 | // TODO retrieve the endpoint 34 | const webSocketEndpoint = endpointArg ?? ''; 35 | 36 | // build and validate pattern 37 | const ajv = new Ajv(); 38 | const validate = ajv.compile(eventPatternSchema); 39 | 40 | // parse and validate the event pattern 41 | const eventPattern = 42 | patternArg !== undefined ? (JSON.parse(patternArg) as unknown) : {}; 43 | 44 | if (!validate(eventPattern)) { 45 | console.error(validate.errors); 46 | throw new Error('Invalid pattern'); 47 | } 48 | 49 | return { credentials, eventPattern, region, webSocketEndpoint }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/event-scout/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | 3 | import { buildArguments } from './buildArguments'; 4 | import { listenToWebSocket } from './listenToWebSocket'; 5 | 6 | const program = new Command() 7 | .option('-p, --aws-profile ', 'aws profile') 8 | .option('-r, --aws-region ', 'aws region') 9 | .option('--endpoint ', 'websocket endpoint') 10 | .option('--pattern ', 'event pattern') 11 | .parse(process.argv); 12 | 13 | const options = program.opts(); 14 | 15 | const { webSocketEndpoint, credentials, region, eventPattern } = 16 | buildArguments(options); 17 | 18 | void listenToWebSocket({ 19 | webSocketEndpoint, 20 | credentials, 21 | region, 22 | eventPattern, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/event-scout/src/listenToWebSocket.ts: -------------------------------------------------------------------------------- 1 | import { Sha256 } from '@aws-crypto/sha256-js'; 2 | import type { AwsCredentialIdentityProvider } from '@aws-sdk/types'; 3 | import { SignatureV4 } from '@smithy/signature-v4'; 4 | import WebSocket from 'ws'; 5 | 6 | import type { EventPattern } from '@event-scout/construct-contracts'; 7 | 8 | type ListenToWebSocketArgs = { 9 | webSocketEndpoint: string; 10 | credentials: AwsCredentialIdentityProvider; 11 | region: string; 12 | eventPattern: EventPattern; 13 | }; 14 | 15 | export const listenToWebSocket = async ({ 16 | webSocketEndpoint, 17 | region, 18 | credentials, 19 | eventPattern, 20 | }: ListenToWebSocketArgs): Promise => { 21 | const service = 'execute-api'; 22 | 23 | const signatureV4 = new SignatureV4({ 24 | service, 25 | region, 26 | credentials, 27 | sha256: Sha256, 28 | }); 29 | await Promise.resolve(webSocketEndpoint); 30 | 31 | const webSocketUrl = new URL(webSocketEndpoint); 32 | 33 | const presignedRequest = await signatureV4.presign({ 34 | method: 'GET', 35 | hostname: webSocketUrl.host, 36 | path: webSocketUrl.pathname, 37 | protocol: webSocketUrl.protocol, 38 | headers: { 39 | host: webSocketUrl.hostname, // compulsory for signature 40 | }, 41 | }); 42 | 43 | const queryParams = presignedRequest.query; 44 | 45 | // we need to update the URL with the signed query from the presigned request 46 | if (queryParams !== undefined) { 47 | const signedSearchParams = new URLSearchParams(); 48 | Object.entries(queryParams).forEach(([key, value]) => { 49 | if (typeof value === 'string') { 50 | signedSearchParams.append(key, value); 51 | } 52 | }); 53 | webSocketUrl.search = signedSearchParams.toString(); 54 | } 55 | 56 | const client = new WebSocket(webSocketUrl); 57 | client.on('open', () => { 58 | console.log('Connection established...'); 59 | 60 | client.send( 61 | JSON.stringify({ 62 | action: 'startTrail', 63 | eventPattern, 64 | }), 65 | ); 66 | }); 67 | 68 | client.on('error', error => console.error('error', error)); 69 | 70 | client.on('message', data => { 71 | const message = data.toLocaleString(); 72 | const formattedMessage = JSON.stringify(JSON.parse(message), null, 4); 73 | console.log(formattedMessage); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/event-scout/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.options.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "emitDeclarationOnly": false, 6 | "noEmit": true, 7 | "resolveJsonModule": true 8 | }, 9 | "include": ["./**/*.ts", "vitest.config.mts"], 10 | "exclude": ["./dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/event-scout/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lambda-assets/.dependency-cruiser.cjs: -------------------------------------------------------------------------------- 1 | const commonDependencyCruiserConfig = require('../../commonConfiguration/dependency-cruiser.config'); 2 | 3 | const path = ['src']; 4 | const pathNot = ['dist']; 5 | 6 | module.exports = commonDependencyCruiserConfig({ path, pathNot }); 7 | -------------------------------------------------------------------------------- /packages/lambda-assets/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | .esbuild 3 | -------------------------------------------------------------------------------- /packages/lambda-assets/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | parserOptions: { 4 | project: ['./tsconfig.json'], 5 | tsconfigRootDir: __dirname, 6 | }, 7 | settings: { 8 | 'import/resolver': { 9 | typescript: { 10 | project: __dirname, 11 | }, 12 | }, 13 | }, 14 | overrides: [ 15 | { 16 | files: ['**/src/**'], 17 | excludedFiles: ['**/__tests__/**', '**/*.test.ts?(x)'], 18 | rules: { 19 | 'import/no-extraneous-dependencies': [ 20 | 'error', 21 | { 22 | devDependencies: true, 23 | optionalDependencies: false, 24 | peerDependencies: true, 25 | }, 26 | ], 27 | }, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /packages/lambda-assets/.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.lintstagedrc.js'); 2 | module.exports = baseConfig; 3 | -------------------------------------------------------------------------------- /packages/lambda-assets/README.md: -------------------------------------------------------------------------------- 1 | # @event-scout/lambda-assets 2 | 3 | Lambda code for EventScout. 4 | 5 | This repository is part of [EventScout](https://github.com/fargito/event-scout). 6 | -------------------------------------------------------------------------------- /packages/lambda-assets/esbuild.build.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import esbuild from 'esbuild'; 3 | import { existsSync, mkdirSync } from 'node:fs'; 4 | import path, { join } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const lambdas = [ 8 | 'startEventsTrail', 9 | 'listEvents', 10 | 'stopEventsTrail', 11 | 'storeEvents', 12 | 'trailGarbageCollector', 13 | 'forwardEvent', 14 | 'onStartTrail', 15 | 'onWebSocketConnect', 16 | 'onWebSocketDisconnect', 17 | ]; 18 | 19 | await esbuild.build({ 20 | entryPoints: lambdas.map(name => `src/${name}.ts`), 21 | outdir: '.esbuild', 22 | bundle: true, 23 | minify: true, 24 | entryNames: '[name]/handler', 25 | keepNames: true, 26 | sourcemap: true, 27 | external: ['@aws-sdk/*'], // since we are on node22, we should get these out-of the box from the runtime 28 | target: 'node22', 29 | platform: 'node', 30 | /** 31 | * Sets the resolution order for esbuild. 32 | * 33 | * In order to enable tree-shaking of packages, we need specify `module` first (ESM) 34 | * Because it defaults to "main" first (CJS, not tree shakeable) 35 | * https://esbuild.github.io/api/#main-fields 36 | */ 37 | mainFields: ['module', 'main'], 38 | }); 39 | 40 | const __filename = fileURLToPath(import.meta.url); 41 | const __dirname = path.dirname(__filename); 42 | 43 | if (!existsSync('dist')) { 44 | mkdirSync('dist'); 45 | } 46 | 47 | for (const lambda of lambdas) { 48 | execSync( 49 | `cd .esbuild/${lambda} && zip -r ${lambda}.zip . && mv ${lambda}.zip ${join(__dirname, 'dist')} && cd ${__dirname}`, 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/lambda-assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-scout/lambda-assets", 3 | "description": "EventScout: lambda code for EventScout", 4 | "version": "0.8.0", 5 | "author": "fargito", 6 | "license": "MIT", 7 | "homepage": "https://github.com/fargito/event-scout", 8 | "bugs": "https://github.com/fargito/event-scout/issues", 9 | "repository": "fargito/event-scout.git", 10 | "keywords": [ 11 | "AWS Lambda", 12 | "EventBridge", 13 | "Serverless" 14 | ], 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "sideEffects": false, 19 | "files": [ 20 | "dist" 21 | ], 22 | "type": "module", 23 | "exports": { 24 | "./startEventsTrail": "./dist/startEventsTrail.zip", 25 | "./listEvents": "./dist/listEvents.zip", 26 | "./stopEventsTrail": "./dist/stopEventsTrail.zip", 27 | "./storeEvents": "./dist/storeEvents.zip", 28 | "./trailGarbageCollector": "./dist/trailGarbageCollector.zip", 29 | "./forwardEvent": "./dist/forwardEvent.zip", 30 | "./onStartTrail": "./dist/onStartTrail.zip", 31 | "./onWebSocketConnect": "./dist/onWebSocketConnect.zip", 32 | "./onWebSocketDisconnect": "./dist/onWebSocketDisconnect.zip" 33 | }, 34 | "scripts": { 35 | "lint-fix": "pnpm linter-base-config --fix", 36 | "lint-fix-all": "pnpm lint-fix .", 37 | "linter-base-config": "eslint --ext=js,ts", 38 | "package": "node ./esbuild.build.js", 39 | "test": "pnpm test-linter && pnpm test-type && pnpm test-unit && pnpm test-circular", 40 | "test-circular": "pnpm depcruise --config -- src", 41 | "test-linter": "pnpm linter-base-config .", 42 | "test-type": "tsc --noEmit --emitDeclarationOnly false", 43 | "test-unit": "vitest run --coverage --passWithNoTests" 44 | }, 45 | "devDependencies": { 46 | "@aws-sdk/client-apigatewaymanagementapi": "3.823.0", 47 | "@aws-sdk/client-dynamodb": "3.823.0", 48 | "@aws-sdk/client-eventbridge": "3.824.0", 49 | "@aws-sdk/lib-dynamodb": "3.823.0", 50 | "@event-scout/construct-contracts": "workspace:^0.8.0", 51 | "@swarmion/serverless-contracts": "0.35.0", 52 | "@swarmion/serverless-helpers": "0.35.0", 53 | "@types/aws-lambda": "8.10.149", 54 | "@types/node": "22.15.29", 55 | "@vitest/coverage-v8": "3.2.2", 56 | "ajv": "8.17.1", 57 | "concurrently": "9.1.2", 58 | "dependency-cruiser": "16.10.2", 59 | "esbuild": "0.25.5", 60 | "eslint": "^8.56.0", 61 | "prettier": "3.5.3", 62 | "typescript": "5.8.3", 63 | "vite-tsconfig-paths": "5.1.4", 64 | "vitest": "3.2.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/lambda-assets/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-assets", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "tags": [], 6 | "implicitDependencies": [] 7 | } 8 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/forwardEvent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiGatewayManagementApiClient, 3 | PostToConnectionCommand, 4 | } from '@aws-sdk/client-apigatewaymanagementapi'; 5 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 6 | import type { EventBridgeEvent } from 'aws-lambda'; 7 | 8 | const webSocketEndpoint = getEnvVariable('WEBSOCKET_ENDPOINT'); 9 | const endpoint = webSocketEndpoint.replace('wss', 'https'); 10 | 11 | const apiGatewayClient = new ApiGatewayManagementApiClient({ endpoint }); 12 | 13 | export const main = async ( 14 | event: EventBridgeEvent & { trailId: string }, 15 | ): Promise => { 16 | const { trailId } = event; 17 | 18 | const data = new TextEncoder().encode(JSON.stringify(event)); 19 | 20 | await apiGatewayClient.send( 21 | new PostToConnectionCommand({ 22 | ConnectionId: trailId, 23 | Data: data, 24 | }), 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/listEvents.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { getHandler, HttpStatusCodes } from '@swarmion/serverless-contracts'; 3 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 4 | import Ajv from 'ajv'; 5 | 6 | import { listEventsContract } from '@event-scout/construct-contracts'; 7 | 8 | import { version } from '../package.json'; 9 | import { buildListAllTrailEvents } from './utils/listAllTrailEvents'; 10 | 11 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 12 | const dynamodbClient = new DynamoDBClient(); 13 | const listAllTrailEvents = buildListAllTrailEvents(dynamodbClient, tableName); 14 | 15 | export const main = getHandler(listEventsContract, { 16 | ajv: new Ajv(), 17 | validateInput: true, 18 | validateOutput: true, 19 | })(async event => { 20 | const { trailId } = event.pathParameters; 21 | 22 | const events = await listAllTrailEvents(trailId); 23 | 24 | return { 25 | statusCode: HttpStatusCodes.OK, 26 | headers: { 'x-event-scout-version': version }, 27 | body: events, 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/onStartTrail.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; 3 | import { UpdateCommand } from '@aws-sdk/lib-dynamodb'; 4 | import { HttpStatusCodes } from '@swarmion/serverless-contracts'; 5 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 6 | import Ajv from 'ajv'; 7 | import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'; 8 | 9 | import { 10 | type StartWebsocketEventTrailBody, 11 | startWebsocketEventTrailBodySchema, 12 | } from '@event-scout/construct-contracts'; 13 | 14 | import { version } from '../package.json'; 15 | import { buildCreateEventBridgeRuleAndTarget } from './utils/createEventBridgeRuleAndTarget'; 16 | 17 | const eventBridgeClient = new EventBridgeClient({}); 18 | const eventBusName = getEnvVariable('EVENT_BUS_NAME'); 19 | const forwardEventLambdaArn = getEnvVariable('FORWARD_EVENT_LAMBDA_ARN'); 20 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 21 | const dynamodbClient = new DynamoDBClient(); 22 | 23 | const createEventBridgeRuleAndTarget = buildCreateEventBridgeRuleAndTarget({ 24 | eventBridgeClient, 25 | eventBusName, 26 | }); 27 | 28 | export const main: APIGatewayProxyWebsocketHandlerV2 = async event => { 29 | const { connectionId: trailId } = event.requestContext; 30 | const { body } = event; 31 | 32 | if (body === undefined) { 33 | throw new Error('No body found'); 34 | } 35 | 36 | const websocketBody: unknown = JSON.parse(body); 37 | 38 | const ajv = new Ajv(); 39 | const validate = ajv.compile( 40 | startWebsocketEventTrailBodySchema, 41 | ); 42 | 43 | if (!validate(websocketBody)) { 44 | console.error(validate.errors); 45 | throw new Error('Payload does not match type StartWebsocketEventTrailBody'); 46 | } 47 | 48 | const { eventPattern } = websocketBody; 49 | 50 | // store an item in DynamoDB to represent the trail. This will enable automatic cleanup 51 | // if the user forget to call the stop route 52 | await dynamodbClient.send( 53 | new UpdateCommand({ 54 | TableName: tableName, 55 | Key: { 56 | PK: trailId, 57 | SK: `TRAIL`, 58 | }, 59 | UpdateExpression: 'SET eventPattern = :eventPattern', 60 | ExpressionAttributeValues: { 61 | ':eventPattern': eventPattern, 62 | }, 63 | }), 64 | ); 65 | 66 | // create the rule and target 67 | await createEventBridgeRuleAndTarget({ 68 | eventPattern, 69 | targetArn: forwardEventLambdaArn, 70 | trailId: trailId, 71 | }); 72 | 73 | return { 74 | statusCode: HttpStatusCodes.OK, 75 | headers: { 'x-event-scout-version': version }, 76 | body: 'Ok', 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/onWebSocketConnect.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { PutCommand } from '@aws-sdk/lib-dynamodb'; 3 | import { HttpStatusCodes } from '@swarmion/serverless-contracts'; 4 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 5 | import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'; 6 | 7 | import { version } from '../package.json'; 8 | 9 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 10 | const dynamodbClient = new DynamoDBClient(); 11 | 12 | export const main: APIGatewayProxyWebsocketHandlerV2 = async event => { 13 | const { connectionId: trailId } = event.requestContext; 14 | 15 | // timestamp must be in seconds 16 | // see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-before-you-start.html 17 | const timeToLive = Date.now() / 1000 + 90 * 60; // 90 minutes 18 | 19 | // store an item in DynamoDB to represent the trail. This will enable automatic cleanup 20 | // if the user forget to call the stop route 21 | await dynamodbClient.send( 22 | new PutCommand({ 23 | TableName: tableName, 24 | Item: { 25 | PK: trailId, 26 | SK: `TRAIL`, 27 | _ttl: timeToLive, 28 | trailId, 29 | }, 30 | }), 31 | ); 32 | 33 | return { 34 | statusCode: HttpStatusCodes.OK, 35 | headers: { 'x-event-scout-version': version }, 36 | body: 'Connected', 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/onWebSocketDisconnect.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; 3 | import { DeleteCommand } from '@aws-sdk/lib-dynamodb'; 4 | import { HttpStatusCodes } from '@swarmion/serverless-contracts'; 5 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 6 | import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'; 7 | 8 | import { version } from '../package.json'; 9 | import { buildDeleteEventBridgeRuleAndTarget } from './utils/deleteEventBridgeRuleAndTarget'; 10 | 11 | const eventBridgeClient = new EventBridgeClient({}); 12 | const eventBusName = getEnvVariable('EVENT_BUS_NAME'); 13 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 14 | const dynamodbClient = new DynamoDBClient(); 15 | 16 | const deleteEventBridgeRuleAndTarget = buildDeleteEventBridgeRuleAndTarget({ 17 | eventBridgeClient, 18 | eventBusName, 19 | }); 20 | 21 | export const main: APIGatewayProxyWebsocketHandlerV2 = async event => { 22 | const { connectionId: trailId } = event.requestContext; 23 | 24 | await deleteEventBridgeRuleAndTarget(trailId); 25 | 26 | // remove the trail item from DynamoDB 27 | await dynamodbClient.send( 28 | new DeleteCommand({ 29 | TableName: tableName, 30 | Key: { PK: trailId, SK: `TRAIL` }, 31 | }), 32 | ); 33 | 34 | return { 35 | statusCode: HttpStatusCodes.OK, 36 | headers: { 'x-event-scout-version': version }, 37 | body: 'Disconnected', 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/startEventsTrail.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; 3 | import { PutCommand } from '@aws-sdk/lib-dynamodb'; 4 | import { getHandler, HttpStatusCodes } from '@swarmion/serverless-contracts'; 5 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 6 | import Ajv from 'ajv'; 7 | import { randomUUID } from 'crypto'; 8 | 9 | import { startEventsTrailContract } from '@event-scout/construct-contracts'; 10 | 11 | import { version } from '../package.json'; 12 | import { buildCreateEventBridgeRuleAndTarget } from './utils/createEventBridgeRuleAndTarget'; 13 | 14 | const eventBridgeClient = new EventBridgeClient({}); 15 | const eventBusName = getEnvVariable('EVENT_BUS_NAME'); 16 | const storeEventsLambdaArn = getEnvVariable('STORE_EVENTS_LAMBDA_ARN'); 17 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 18 | const dynamodbClient = new DynamoDBClient(); 19 | 20 | const createEventBridgeRuleAndTarget = buildCreateEventBridgeRuleAndTarget({ 21 | eventBridgeClient, 22 | eventBusName, 23 | }); 24 | 25 | export const main = getHandler(startEventsTrailContract, { 26 | ajv: new Ajv(), 27 | validateInput: true, 28 | validateOutput: true, 29 | })(async event => { 30 | const { eventPattern } = event.body; 31 | 32 | const trailId = randomUUID(); 33 | 34 | // timestamp must be in seconds 35 | // see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-before-you-start.html 36 | const timeToLive = Date.now() / 1000 + 15 * 60; // 15 minutes 37 | 38 | // store an item in DynamoDB to represent the trail. This will enable automatic cleanup 39 | // if the user forget to call the stop route 40 | await dynamodbClient.send( 41 | new PutCommand({ 42 | TableName: tableName, 43 | Item: { PK: trailId, SK: `TRAIL`, _ttl: timeToLive, trailId }, 44 | }), 45 | ); 46 | 47 | // create the rule and target 48 | await createEventBridgeRuleAndTarget({ 49 | eventPattern, 50 | targetArn: storeEventsLambdaArn, 51 | trailId, 52 | }); 53 | 54 | return { 55 | statusCode: HttpStatusCodes.OK, 56 | headers: { 'x-event-scout-version': version }, 57 | body: { trailId }, 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/stopEventsTrail.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; 3 | import { DeleteCommand } from '@aws-sdk/lib-dynamodb'; 4 | import { getHandler, HttpStatusCodes } from '@swarmion/serverless-contracts'; 5 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 6 | import Ajv from 'ajv'; 7 | 8 | import { stopEventsTrailContract } from '@event-scout/construct-contracts'; 9 | 10 | import { version } from '../package.json'; 11 | import { buildDeleteEventBridgeRuleAndTarget } from './utils/deleteEventBridgeRuleAndTarget'; 12 | 13 | const eventBridgeClient = new EventBridgeClient({}); 14 | const eventBusName = getEnvVariable('EVENT_BUS_NAME'); 15 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 16 | const dynamodbClient = new DynamoDBClient(); 17 | 18 | const deleteEventBridgeRuleAndTarget = buildDeleteEventBridgeRuleAndTarget({ 19 | eventBridgeClient, 20 | eventBusName, 21 | }); 22 | 23 | export const main = getHandler(stopEventsTrailContract, { 24 | ajv: new Ajv(), 25 | validateInput: true, 26 | validateOutput: true, 27 | })(async event => { 28 | const { trailId } = event.body; 29 | 30 | await deleteEventBridgeRuleAndTarget(trailId); 31 | 32 | // remove the trail item from DynamoDB 33 | await dynamodbClient.send( 34 | new DeleteCommand({ 35 | TableName: tableName, 36 | Key: { PK: trailId, SK: `TRAIL` }, 37 | }), 38 | ); 39 | 40 | return { 41 | statusCode: HttpStatusCodes.OK, 42 | headers: { 'x-event-scout-version': version }, 43 | body: { trailId }, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/storeEvents.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { PutCommand } from '@aws-sdk/lib-dynamodb'; 3 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 4 | import type { EventBridgeEvent } from 'aws-lambda'; 5 | 6 | const tableName = getEnvVariable('EVENT_SCOUT_TABLE_NAME'); 7 | const dynamodbClient = new DynamoDBClient(); 8 | 9 | export const main = async ( 10 | event: EventBridgeEvent & { trailId: string }, 11 | ): Promise => { 12 | // timestamp must be in seconds 13 | // see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-before-you-start.html 14 | const timeToLive = Date.now() / 1000 + 15 * 60; // 15 minutes 15 | 16 | await dynamodbClient.send( 17 | new PutCommand({ 18 | TableName: tableName, 19 | Item: { 20 | event, 21 | PK: event.trailId, 22 | SK: `EVENT#${event.time}#${event.id}`, 23 | _ttl: timeToLive, 24 | }, 25 | }), 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/trailGarbageCollector.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeClient } from '@aws-sdk/client-eventbridge'; 2 | import { getEnvVariable } from '@swarmion/serverless-helpers'; 3 | import type { DynamoDBStreamEvent } from 'aws-lambda'; 4 | 5 | import { buildDeleteEventBridgeRuleAndTarget } from './utils/deleteEventBridgeRuleAndTarget'; 6 | 7 | const eventBridgeClient = new EventBridgeClient({}); 8 | const eventBusName = getEnvVariable('EVENT_BUS_NAME'); 9 | const deleteEventBridgeRuleAndTarget = buildDeleteEventBridgeRuleAndTarget({ 10 | eventBridgeClient, 11 | eventBusName, 12 | }); 13 | 14 | export const main = async (event: DynamoDBStreamEvent): Promise => { 15 | const trailId = event.Records[0]?.dynamodb?.OldImage?.trailId?.S; 16 | 17 | if (trailId === undefined) throw new Error('Unable to find trailId'); 18 | 19 | await deleteEventBridgeRuleAndTarget(trailId); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/lambda-assets/src/utils/createEventBridgeRuleAndTarget.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventBridgeClient, 3 | PutRuleCommand, 4 | PutTargetsCommand, 5 | } from '@aws-sdk/client-eventbridge'; 6 | 7 | import type { EventPattern } from '@event-scout/construct-contracts'; 8 | 9 | import { getRuleAndTargetName } from './getRuleAndTargetName'; 10 | 11 | type CreateEventBridgeRuleAndTarget = (args: { 12 | trailId: string; 13 | targetArn: string; 14 | eventPattern: EventPattern; 15 | }) => Promise; 16 | 17 | type Args = { 18 | eventBridgeClient: EventBridgeClient; 19 | eventBusName: string; 20 | }; 21 | 22 | export const buildCreateEventBridgeRuleAndTarget = 23 | ({ eventBridgeClient, eventBusName }: Args): CreateEventBridgeRuleAndTarget => 24 | async ({ trailId, targetArn, eventPattern }) => { 25 | const { ruleName, targetName } = getRuleAndTargetName(trailId); 26 | 27 | // put a new rule 28 | await eventBridgeClient.send( 29 | new PutRuleCommand({ 30 | EventBusName: eventBusName, 31 | Name: ruleName, 32 | EventPattern: JSON.stringify(eventPattern), 33 | }), 34 | ); 35 | 36 | // create a new target 37 | await eventBridgeClient.send( 38 | new PutTargetsCommand({ 39 | EventBusName: eventBusName, 40 | Rule: ruleName, 41 | Targets: [ 42 | { 43 | Id: targetName, 44 | Arn: targetArn, 45 | // https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html 46 | InputTransformer: { 47 | InputTemplate: ` 48 | { 49 | "id": , 50 | "version": , 51 | "account": , 52 | "time":