diff --git a/.eslintrc.js b/.eslintrc.js index 84ba0b7..3e9bea3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,6 +39,34 @@ module.exports = { jest: false, node: true } + }, + { + // The core is framework-independent, and its main entry point loads + // no optional peer: those get entry points of their own. + files: ['projects/ui-watchtower/src/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: [ + '@angular/*', + 'zone.js', + '@absaoss-cps/ngx-ui-watchtower', + '@absaoss-cps/ngx-ui-watchtower/*' + ], + message: 'The core is framework-independent.' + }, + { + group: ['rxjs', 'rxjs/*', 'aws-rum-web', 'aws-rum-web/*'], + message: + 'The main entry point loads no optional peer; use an entry point of its own.' + } + ] + } + ] + } } ] }; diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d0eadcb..d32c598 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -60,7 +60,7 @@ jobs: run: npm run test:coverage - name: Comment coverage on PR - uses: ArtiomTr/jest-coverage-report-action@v2 + uses: ArtiomTr/jest-coverage-report-action@262a7bb0b20c4d1d6b6b026af0f008f78da72788 # v2.3.1 if: github.event_name == 'pull_request' continue-on-error: true with: @@ -69,6 +69,18 @@ jobs: # Tests already ran above; the action only formats the report. skip-step: all + - name: Comment core coverage on PR + uses: ArtiomTr/jest-coverage-report-action@262a7bb0b20c4d1d6b6b026af0f008f78da72788 # v2.3.1 + if: github.event_name == 'pull_request' + continue-on-error: true + with: + custom-title: Core coverage report + coverage-file: ./coverage/ui-watchtower/report.json + # The same file as the base: no comparison with a base branch, + # which would run the Angular package's tests. + base-coverage-file: ./coverage/ui-watchtower/report.json + skip-step: all + lint: name: Lint and format runs-on: ubuntu-latest diff --git a/README.md b/README.md index 91ba9b6..33008ab 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ Source of [`@absaoss-cps/ngx-ui-watchtower`](https://www.npmjs.com/package/@absaoss-cps/ngx-ui-watchtower), a telemetry library for Angular apps: scenarios (user end-to-end journeys or specific functional workflows through an app), BI events and -logs, with an optional AWS CloudWatch RUM sink. +logs, with an optional AWS CloudWatch RUM sink. It builds on +[`@absaoss-cps/ui-watchtower`](projects/ui-watchtower/README.md), the +framework-independent engine, which lives here too. How to use the library: [projects/ngx-ui-watchtower/README.md](projects/ngx-ui-watchtower/README.md) (also the npm page). @@ -19,14 +21,22 @@ How it is designed: [projects/ngx-ui-watchtower/DESIGN.md](projects/ngx-ui-watch ## Layout ```text +projects/ui-watchtower/ + src/ @absaoss-cps/ui-watchtower — the framework-independent engine + README.md usage guide, published to npm + package.json the published package's manifest projects/ngx-ui-watchtower/ src/ main entry point — @absaoss-cps/ngx-ui-watchtower rum/ secondary entry point — @absaoss-cps/ngx-ui-watchtower/rum + type-tests/ consumer type tests README.md usage guide, published to npm DESIGN.md full design package.json the published package's manifest +scripts/ the public API snapshot ``` +The Angular package depends on the core, never the other way round. + ## Getting started Requires Node.js 22 or later. @@ -37,22 +47,25 @@ npm run test npm run build ``` -The package is built to `dist/ngx-ui-watchtower`. +The packages are built to `dist/ui-watchtower` and `dist/ngx-ui-watchtower`, +the core first: the Angular package compiles against the built core, as an +application does. Tests and the typecheck read the core's source. ## Scripts -| Script | What it does | -| ----------------------- | --------------------------------------------------------------- | -| `npm run build` | Builds the package (production, partial compilation) | -| `npm run watch` | Rebuilds on change (development) | -| `npm run test` | Runs the unit tests (Jest) | -| `npm run test:coverage` | Runs the unit tests with coverage, as CI does | -| `npm run typecheck` | Type-checks every library and spec tsconfig, and the type tests | -| `npm run api:check` | Builds, then compares the public API with its snapshot | -| `npm run api:update` | Builds, then rewrites the snapshot, for an intended change | -| `npm run lint` | ESLint, no warnings allowed | -| `npm run format` | Formats with Prettier | -| `npm run format:check` | Checks formatting without writing | +| Script | What it does | +| ----------------------- | -------------------------------------------------------------- | +| `npm run build` | Builds both packages (production), the core first | +| `npm run watch` | Builds the core, then rebuilds the Angular package on change | +| `npm run test` | Runs the unit tests of both packages (Jest) | +| `npm run test:core` | Runs the core's unit tests only: plain Jest, no Angular | +| `npm run test:coverage` | Runs the unit tests with coverage, as CI does | +| `npm run typecheck` | Builds the core, then type-checks every tsconfig and type test | +| `npm run api:check` | Builds both packages, then compares their public APIs | +| `npm run api:update` | Builds both packages, then rewrites their snapshots | +| `npm run lint` | ESLint, no warnings allowed | +| `npm run format` | Formats with Prettier | +| `npm run format:check` | Checks formatting without writing | Three checks guard what consumers depend on: @@ -60,9 +73,8 @@ Three checks guard what consumers depend on: Angular compiler, a consumer that uses every public export, and one that augments the name registries: registered names accepted, others rejected, `string` while nothing is registered. -- **The API snapshot** (`projects/ngx-ui-watchtower/public-api.snapshot.json`) - records every export of the built package with its members and - signatures. After an intended change, run `npm run api:update` and review +- **The API snapshots** (`public-api.snapshot.json` in each package) record + every export of the built packages with its members and signatures. After an intended change, run `npm run api:update` and review the diff. - **The broadcast wire fixture** (`projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast.wire.fixture.json`) records diff --git a/angular.json b/angular.json index 9cbcf28..b104002 100644 --- a/angular.json +++ b/angular.json @@ -7,6 +7,29 @@ "analytics": false }, "projects": { + "ui-watchtower": { + "projectType": "library", + "root": "projects/ui-watchtower", + "sourceRoot": "projects/ui-watchtower/src", + "prefix": "uwt", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "options": { + "project": "projects/ui-watchtower/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ui-watchtower/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ui-watchtower/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + } + } + }, "ngx-ui-watchtower": { "projectType": "library", "root": "projects/ngx-ui-watchtower", diff --git a/jest.config.js b/jest.config.js index 3a8a9c8..76484a4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,10 @@ module.exports = { - roots: ['/projects'], + roots: ['/projects/ngx-ui-watchtower'], coverageDirectory: '/coverage', preset: 'jest-preset-angular', moduleNameMapper: { + '^@absaoss-cps/ui-watchtower$': + '/projects/ui-watchtower/src/public-api.ts', '^@absaoss-cps/ngx-ui-watchtower$': '/projects/ngx-ui-watchtower/src/public-api.ts', '^@absaoss-cps/ngx-ui-watchtower/rum$': diff --git a/package-lock.json b/package-lock.json index 8aaef95..66a8b84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "jest-preset-angular": "^17.0.0", "ng-packagr": "~21.0.1", "prettier": "^3.4.2", + "ts-jest": "^29.4.14", "typescript": "~5.9.3" }, "engines": { diff --git a/package.json b/package.json index 0bffd83..f95fdc3 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "build": "ng build --configuration=production --project=ngx-ui-watchtower", - "watch": "ng build ngx-ui-watchtower --watch --configuration development", - "test": "ng test --project=ngx-ui-watchtower", - "test:coverage": "ng test --project=ngx-ui-watchtower --coverage --coverage-reporters text-summary --coverage-reporters lcov --json --output-file=./coverage/report.json", + "build": "ng build --configuration=production --project=ui-watchtower && ng build --configuration=production --project=ngx-ui-watchtower", + "watch": "ng build ui-watchtower --configuration development && ng build ngx-ui-watchtower --watch --configuration development", + "test": "npm run test:core && ng test --project=ngx-ui-watchtower", + "test:core": "jest --config projects/ui-watchtower/jest.config.js", + "test:coverage": "jest --config projects/ui-watchtower/jest.config.js --coverage --coverageReporters=text-summary --coverageReporters=lcov --json --outputFile=./coverage/ui-watchtower/report.json && ng test --project=ngx-ui-watchtower --coverage --coverage-reporters text-summary --coverage-reporters lcov --json --output-file=./coverage/report.json", "lint": "eslint \"**/*.ts\" --max-warnings=0", "format": "prettier --write \"**/*.{ts,mjs,html}\"", "format:check": "prettier --check \"**/*.{ts,mjs,html}\"", - "typecheck": "tsc -p projects/ngx-ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/tsconfig.spec.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.spec.json --noEmit && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.json && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.augmented.json", + "typecheck": "tsc -p projects/ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ui-watchtower/tsconfig.spec.json --noEmit && ng build --configuration=production --project=ui-watchtower && tsc -p projects/ngx-ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/tsconfig.spec.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.spec.json --noEmit && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.json && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.augmented.json", "api:check": "npm run build && node scripts/api-snapshot.mjs", "api:update": "npm run build && node scripts/api-snapshot.mjs --update" }, @@ -48,6 +49,7 @@ "jest-preset-angular": "^17.0.0", "ng-packagr": "~21.0.1", "prettier": "^3.4.2", + "ts-jest": "^29.4.14", "typescript": "~5.9.3" }, "engines": { diff --git a/projects/ngx-ui-watchtower/DESIGN.md b/projects/ngx-ui-watchtower/DESIGN.md index 0e6eb0d..83bc522 100644 --- a/projects/ngx-ui-watchtower/DESIGN.md +++ b/projects/ngx-ui-watchtower/DESIGN.md @@ -192,6 +192,13 @@ providers: [ ### Packaging and internals +- **The framework-independent code lives in `@absaoss-cps/ui-watchtower`.** + This package depends on it as a regular dependency, so installing this + package installs it too, and re-exports what it has always exported, under + the same names. The dependency runs one way: the core imports neither + Angular nor this package, which a lint rule enforces. The library build + compiles against the core's built declarations, not its source, so it sees + the core exactly as an application does. - **Every export is explicit.** The barrels list their exports one by one — in a published package each exported name is a compatibility promise. Id generation, the clock, the fail-open wrappers, User Timings and the diff --git a/projects/ngx-ui-watchtower/README.md b/projects/ngx-ui-watchtower/README.md index 9d8c910..b3a1864 100644 --- a/projects/ngx-ui-watchtower/README.md +++ b/projects/ngx-ui-watchtower/README.md @@ -26,7 +26,10 @@ npm install @absaoss-cps/ngx-ui-watchtower npm install aws-rum-web # only for the AWS RUM sink ``` -Requires Angular 21 or 22 and RxJS 7.8. +Requires Angular 21 or 22 and RxJS 7.8. It brings +[`@absaoss-cps/ui-watchtower`](https://www.npmjs.com/package/@absaoss-cps/ui-watchtower), +the framework-independent engine it builds on, with it: there is nothing more +to install or configure. ## Setup diff --git a/projects/ngx-ui-watchtower/ng-package.json b/projects/ngx-ui-watchtower/ng-package.json index ff8424f..2e407db 100644 --- a/projects/ngx-ui-watchtower/ng-package.json +++ b/projects/ngx-ui-watchtower/ng-package.json @@ -3,5 +3,6 @@ "dest": "../../dist/ngx-ui-watchtower", "lib": { "entryFile": "src/public-api.ts" - } + }, + "allowedNonPeerDependencies": ["@absaoss-cps/ui-watchtower"] } diff --git a/projects/ngx-ui-watchtower/package.json b/projects/ngx-ui-watchtower/package.json index 604c14b..58bd169 100644 --- a/projects/ngx-ui-watchtower/package.json +++ b/projects/ngx-ui-watchtower/package.json @@ -20,6 +20,7 @@ "homepage": "https://github.com/AbsaOSS/ui-watchtower", "license": "Apache-2.0", "dependencies": { + "@absaoss-cps/ui-watchtower": "^0.1.0", "tslib": "^2.8.1" }, "sideEffects": false, diff --git a/projects/ngx-ui-watchtower/rum/src/lib/uwt-rum-telemetry.sink/uwt-rum-telemetry.sink.spec.ts b/projects/ngx-ui-watchtower/rum/src/lib/uwt-rum-telemetry.sink/uwt-rum-telemetry.sink.spec.ts index 2de1462..6a5b32f 100644 --- a/projects/ngx-ui-watchtower/rum/src/lib/uwt-rum-telemetry.sink/uwt-rum-telemetry.sink.spec.ts +++ b/projects/ngx-ui-watchtower/rum/src/lib/uwt-rum-telemetry.sink/uwt-rum-telemetry.sink.spec.ts @@ -7,7 +7,7 @@ import { UWT_TELEMETRY_EVENT_TYPE, UWT_TELEMETRY_IDENTITY } from '@absaoss-cps/ngx-ui-watchtower'; -import { uwtSessionElapsed } from '../../../../src/lib/utils/uwt-session-start.util/uwt-session-start.util'; +import { uwtSessionElapsed } from '@absaoss-cps/ui-watchtower'; import { UWT_RUM_CREDENTIALS_PROVIDER, UwtRumBootstrap diff --git a/projects/ngx-ui-watchtower/rum/tsconfig.lib.json b/projects/ngx-ui-watchtower/rum/tsconfig.lib.json index 06ed2d1..9435509 100644 --- a/projects/ngx-ui-watchtower/rum/tsconfig.lib.json +++ b/projects/ngx-ui-watchtower/rum/tsconfig.lib.json @@ -3,7 +3,8 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "../../../out-tsc/lib-rum", - "rootDir": "..", + // Includes the core: tests and typecheck read its source. + "rootDir": "../..", "lib": ["ES2022", "DOM"], "moduleResolution": "bundler", "declaration": true, diff --git a/projects/ngx-ui-watchtower/rum/tsconfig.spec.json b/projects/ngx-ui-watchtower/rum/tsconfig.spec.json index 2e29a92..2677ee6 100644 --- a/projects/ngx-ui-watchtower/rum/tsconfig.spec.json +++ b/projects/ngx-ui-watchtower/rum/tsconfig.spec.json @@ -3,7 +3,8 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "../../../out-tsc/spec-rum", - "rootDir": "..", + // Includes the core: tests and typecheck read its source. + "rootDir": "../..", "types": ["jest"], "resolveJsonModule": true, "isolatedModules": false diff --git a/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.spec.ts b/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.spec.ts index 5d7ac0b..1de060c 100644 --- a/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.spec.ts +++ b/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.spec.ts @@ -3,7 +3,7 @@ import { UwtScenarioRecord } from '../../models/uwt-scenario.models/uwt-scenario import { UwtTelemetryMetadata } from '../../models/uwt-telemetry-common.models/uwt-telemetry-common.models'; import { UwtTelemetrySink } from '../../sinks/uwt-telemetry/uwt-telemetry-abstract.sink/uwt-telemetry-abstract.sink'; import { UWT_DEFAULT_REDACT_CONFIG } from '../../utils/uwt-telemetry-redact.util/uwt-telemetry-redact.util'; -import { uwtMarkSessionStart } from '../../utils/uwt-session-start.util/uwt-session-start.util'; +import { uwtMarkSessionStart } from '@absaoss-cps/ui-watchtower'; import { UwtScenario, UwtScenarioDeps } from './uwt-scenario'; /** Captures what the scenario emitted, so a test can assert on it. */ diff --git a/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.ts b/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.ts index 636547c..0e32b2b 100644 --- a/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.ts +++ b/projects/ngx-ui-watchtower/src/lib/scenario/uwt-scenario/uwt-scenario.ts @@ -30,7 +30,7 @@ import { uwtRedactMetadata, uwtScrubString } from '../../utils/uwt-telemetry-redact.util/uwt-telemetry-redact.util'; -import { uwtSessionElapsed } from '../../utils/uwt-session-start.util/uwt-session-start.util'; +import { uwtSessionElapsed } from '@absaoss-cps/ui-watchtower'; import { uwtDeepClone, uwtElapsedNow, diff --git a/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast-host.service.ts b/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast-host.service.ts index 22a781f..36a0cd4 100644 --- a/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast-host.service.ts +++ b/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast-host.service.ts @@ -20,7 +20,7 @@ import { UwtLogQuery } from '../../providers/uwt-log-api.provider/uwt-log-api.provider'; import { uwtClassifyTelemetryEvent } from '../../utils/uwt-telemetry-event.util/uwt-telemetry-event.util'; -import { uwtSessionElapsed } from '../../utils/uwt-session-start.util/uwt-session-start.util'; +import { uwtSessionElapsed } from '@absaoss-cps/ui-watchtower'; /** The identity fields scenario, BI and log records carry. */ interface UwtIdentified { diff --git a/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast.spec.ts b/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast.spec.ts index bd4adfb..5a1996b 100644 --- a/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast.spec.ts +++ b/projects/ngx-ui-watchtower/src/lib/sinks/uwt-broadcast/uwt-broadcast.spec.ts @@ -36,7 +36,7 @@ import { uwtIsBroadcastMessage } from './uwt-broadcast.messages'; import { UwtTelemetrySink } from '../uwt-telemetry/uwt-telemetry-abstract.sink/uwt-telemetry-abstract.sink'; -import { uwtMarkSessionStart } from '../../utils/uwt-session-start.util/uwt-session-start.util'; +import { uwtMarkSessionStart } from '@absaoss-cps/ui-watchtower'; /** * Each realm gets its own injector — the shell and every fragment run in a diff --git a/projects/ngx-ui-watchtower/src/public-api.ts b/projects/ngx-ui-watchtower/src/public-api.ts index 795967e..5219755 100644 --- a/projects/ngx-ui-watchtower/src/public-api.ts +++ b/projects/ngx-ui-watchtower/src/public-api.ts @@ -110,7 +110,7 @@ export type { UwtTraceScenarioOptions } from './lib/scenario/uwt-scenario-operat export { uwtIsDebugEnabled } from './lib/utils/uwt-debug-flag.util/uwt-debug-flag.util'; export type { UwtDebugFlag } from './lib/utils/uwt-debug-flag.util/uwt-debug-flag.util'; export { uwtUuid } from './lib/utils/uwt-telemetry-safe.util/uwt-telemetry-safe.util'; -export { uwtMarkSessionStart } from './lib/utils/uwt-session-start.util/uwt-session-start.util'; +export { uwtMarkSessionStart } from '@absaoss-cps/ui-watchtower'; export { UWT_DEFAULT_REDACT_CONFIG, UWT_REDACTED, diff --git a/projects/ngx-ui-watchtower/tsconfig.lib.json b/projects/ngx-ui-watchtower/tsconfig.lib.json index 44d58cd..db304db 100644 --- a/projects/ngx-ui-watchtower/tsconfig.lib.json +++ b/projects/ngx-ui-watchtower/tsconfig.lib.json @@ -13,7 +13,11 @@ "skipLibCheck": true, "isolatedModules": false, "types": [], - "paths": {} + // The core as published: its built declarations, not its source. Build + // it first (npm run build does). + "paths": { + "@absaoss-cps/ui-watchtower": ["../../dist/ui-watchtower"] + } }, "include": ["src/**/*.ts"], "exclude": ["**/*.spec.ts"], diff --git a/projects/ngx-ui-watchtower/tsconfig.spec.json b/projects/ngx-ui-watchtower/tsconfig.spec.json index 1748c35..0a82408 100644 --- a/projects/ngx-ui-watchtower/tsconfig.spec.json +++ b/projects/ngx-ui-watchtower/tsconfig.spec.json @@ -3,7 +3,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "rootDir": "./src", + // Includes the core: tests and typecheck read its source. + "rootDir": "..", "types": ["jest", "node"], "resolveJsonModule": true }, diff --git a/projects/ui-watchtower/LICENSE b/projects/ui-watchtower/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/projects/ui-watchtower/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/projects/ui-watchtower/README.md b/projects/ui-watchtower/README.md new file mode 100644 index 0000000..2ea939a --- /dev/null +++ b/projects/ui-watchtower/README.md @@ -0,0 +1,29 @@ +# ui-watchtower + +The framework-independent engine behind +[`@absaoss-cps/ngx-ui-watchtower`](https://www.npmjs.com/package/@absaoss-cps/ngx-ui-watchtower): +scenario health telemetry, business/UX events and application logs, for any +JavaScript or TypeScript application. + +> **Work in progress.** The engine is moving here from the Angular package in +> stages. Until it has, use `@absaoss-cps/ngx-ui-watchtower`; it installs this +> package by itself. + +It imports no framework and touches no browser global when imported, so it +loads in Node, on a server and in a worker as well as in a browser. + +## Install + +```bash +npm install @absaoss-cps/ui-watchtower +``` + +## API + +| Export | What it does | +| -------------------------------------- | ---------------------------------------------------------------------- | +| `uwtMarkSessionStart(sessionId, now?)` | Records when a session started, for the destination that owns sessions | + +## License + +Apache-2.0 diff --git a/projects/ui-watchtower/jest.config.js b/projects/ui-watchtower/jest.config.js new file mode 100644 index 0000000..992afc8 --- /dev/null +++ b/projects/ui-watchtower/jest.config.js @@ -0,0 +1,24 @@ +/** + * Unit tests of the framework-independent core: plain Jest and ts-jest, + * without Angular or zone.js. The environment is Node, so nothing can lean + * on browser globals by accident; a spec that needs them opts into jsdom + * with a `@jest-environment jsdom` docblock. + */ +module.exports = { + displayName: 'ui-watchtower', + rootDir: __dirname, + roots: ['/src'], + testEnvironment: 'node', + transform: { + '^.+\\.ts$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleNameMapper: { + '^@absaoss-cps/ui-watchtower$': '/src/public-api.ts' + }, + coverageDirectory: '/../../coverage/ui-watchtower', + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.spec.ts', + '!src/**/public-api.ts' + ] +}; diff --git a/projects/ui-watchtower/ng-package.json b/projects/ui-watchtower/ng-package.json new file mode 100644 index 0000000..6bfafcd --- /dev/null +++ b/projects/ui-watchtower/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/ui-watchtower", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/ui-watchtower/package.json b/projects/ui-watchtower/package.json new file mode 100644 index 0000000..bc739c4 --- /dev/null +++ b/projects/ui-watchtower/package.json @@ -0,0 +1,30 @@ +{ + "name": "@absaoss-cps/ui-watchtower", + "version": "0.1.0", + "description": "Framework-independent telemetry engine: application logs, scenario health telemetry and business/UX events, for any JavaScript or TypeScript application.", + "repository": { + "type": "git", + "url": "https://github.com/AbsaOSS/ui-watchtower" + }, + "homepage": "https://github.com/AbsaOSS/ui-watchtower", + "license": "Apache-2.0", + "peerDependencies": { + "aws-rum-web": "^3.2.1", + "rxjs": "^7.8.2" + }, + "peerDependenciesMeta": { + "aws-rum-web": { + "optional": true + }, + "rxjs": { + "optional": true + } + }, + "dependencies": { + "tslib": "^2.8.1" + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + } +} diff --git a/projects/ui-watchtower/public-api.snapshot.json b/projects/ui-watchtower/public-api.snapshot.json new file mode 100644 index 0000000..48abe79 --- /dev/null +++ b/projects/ui-watchtower/public-api.snapshot.json @@ -0,0 +1,10 @@ +{ + "@absaoss-cps/ui-watchtower": { + "uwtMarkSessionStart": [ + "function(sessionId: string, now?: number | undefined): void" + ], + "uwtSessionElapsed": [ + "function(sessionId: string | undefined, now?: number | undefined): number | undefined" + ] + } +} diff --git a/projects/ngx-ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts b/projects/ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts similarity index 98% rename from projects/ngx-ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts rename to projects/ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts index f57add8..fe7dea3 100644 --- a/projects/ngx-ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts +++ b/projects/ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.spec.ts @@ -1,3 +1,6 @@ +/** + * @jest-environment jsdom + */ import { uwtMarkSessionStart, uwtSessionElapsed diff --git a/projects/ngx-ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.ts b/projects/ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.ts similarity index 100% rename from projects/ngx-ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.ts rename to projects/ui-watchtower/src/lib/utils/uwt-session-start.util/uwt-session-start.util.ts diff --git a/projects/ui-watchtower/src/public-api.spec.ts b/projects/ui-watchtower/src/public-api.spec.ts new file mode 100644 index 0000000..b167832 --- /dev/null +++ b/projects/ui-watchtower/src/public-api.spec.ts @@ -0,0 +1,20 @@ +import * as core from './public-api'; + +/** + * The core must import, and run, where there is no browser: in Node, on a + * server, in a worker. This spec runs in Jest's Node environment, with no + * `window`, `document` or `localStorage`. + */ +describe('the core outside a browser', () => { + it('should import without browser globals', () => { + expect(typeof window).toBe('undefined'); + expect(typeof document).toBe('undefined'); + expect(typeof localStorage).toBe('undefined'); + expect(Object.keys(core)).toContain('uwtMarkSessionStart'); + }); + + it('should keep no session start, and not throw, without storage', () => { + expect(() => core.uwtMarkSessionStart('s1', 1_000)).not.toThrow(); + expect(core.uwtSessionElapsed('s1', 2_000)).toBeUndefined(); + }); +}); diff --git a/projects/ui-watchtower/src/public-api.ts b/projects/ui-watchtower/src/public-api.ts new file mode 100644 index 0000000..abdf86a --- /dev/null +++ b/projects/ui-watchtower/src/public-api.ts @@ -0,0 +1,13 @@ +/* + * Public API Surface of ui-watchtower + * + * The framework-independent engine. Nothing here imports a framework, and + * nothing touches a browser global when imported. + */ + +// Utilities a destination needs +export { uwtMarkSessionStart } from './lib/utils/uwt-session-start.util/uwt-session-start.util'; + +// Used by @absaoss-cps/ngx-ui-watchtower while the engine moves into this +// package; removed from the public API once that code lives here too. +export { uwtSessionElapsed } from './lib/utils/uwt-session-start.util/uwt-session-start.util'; diff --git a/projects/ui-watchtower/tsconfig.lib.json b/projects/ui-watchtower/tsconfig.lib.json new file mode 100644 index 0000000..2e0fb23 --- /dev/null +++ b/projects/ui-watchtower/tsconfig.lib.json @@ -0,0 +1,22 @@ +/* + * The core library: plain TypeScript, built by ng-packagr like the Angular + * package, but with no Angular in it. + */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib-core", + "rootDir": "./src", + "lib": ["ES2022", "DOM"], + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "sourceMap": true, + "skipLibCheck": true, + "isolatedModules": false, + "types": [], + "paths": {} + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/projects/ui-watchtower/tsconfig.lib.prod.json b/projects/ui-watchtower/tsconfig.lib.prod.json new file mode 100644 index 0000000..2a2faa8 --- /dev/null +++ b/projects/ui-watchtower/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/projects/ui-watchtower/tsconfig.spec.json b/projects/ui-watchtower/tsconfig.spec.json new file mode 100644 index 0000000..f0716fe --- /dev/null +++ b/projects/ui-watchtower/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec-core", + "rootDir": "./src", + "types": ["jest", "node"], + "isolatedModules": false + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/scripts/api-snapshot.mjs b/scripts/api-snapshot.mjs index 140fc26..2561b8c 100644 --- a/scripts/api-snapshot.mjs +++ b/scripts/api-snapshot.mjs @@ -19,6 +19,10 @@ import ts from 'typescript'; const ROOT = fileURLToPath(new URL('..', import.meta.url)); const PACKAGES = [ + { + dist: 'dist/ui-watchtower', + snapshot: 'projects/ui-watchtower/public-api.snapshot.json' + }, { dist: 'dist/ngx-ui-watchtower', snapshot: 'projects/ngx-ui-watchtower/public-api.snapshot.json' diff --git a/tsconfig.json b/tsconfig.json index 9e6331e..24ff742 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,9 @@ "compilerOptions": { "resolveJsonModule": true, "paths": { + "@absaoss-cps/ui-watchtower": [ + "./projects/ui-watchtower/src/public-api.ts" + ], "@absaoss-cps/ngx-ui-watchtower": [ "./projects/ngx-ui-watchtower/src/public-api.ts" ], @@ -39,6 +42,8 @@ }, "files": [], "references": [ + { "path": "./projects/ui-watchtower/tsconfig.lib.json" }, + { "path": "./projects/ui-watchtower/tsconfig.spec.json" }, { "path": "./projects/ngx-ui-watchtower/tsconfig.lib.json" }, { "path": "./projects/ngx-ui-watchtower/tsconfig.spec.json" }, { "path": "./projects/ngx-ui-watchtower/rum/tsconfig.lib.json" },