UNPKG

react-native-avo-inspector

Version:

[![npm version](https://badge.fury.io/js/react-native-avo-inspector.svg)](https://badge.fury.io/js/react-native-avo-inspector)

347 lines (261 loc) 20.1 kB
# PRD: Lite Entry Point **Feature:** lite-entry-point **Branch:** feat/lite-entry-point **Base:** main **Date:** 2026-03-18 **Spec:** planning/lite-entry-point/spec.md (Rev 4) **Spec review:** planning/lite-entry-point/spec-review.md (Rev 4/5, score 95/100) **Status:** Draft — Rev 2 (Thing Rev 1 fixes) --- ## Background The current `avo-inspector` package always ships `@noble/curves` (P-256 elliptic curve) and the event-spec validation pipeline. These are required only when a `publicEncryptionKey` is provided and the inspector is in dev/staging mode. Customers who never use property-value encryption pay a bundle-size cost for code they cannot use. The lite entry point ships a stripped build (`avo-inspector/lite`) that omits: - `AvoEncryption.ts` and `@noble/curves` - `AvoEventSpecFetcher`, `AvoEventSpecCache`, `EventValidator`, `safe-regex2` Target: under 7 KB gzipped after terser. The full entry point is unchanged. --- ## Architecture Summary Six new source files in `src/lite/` are created as minimal copies of their full counterparts. Each copy changes exactly one import statement — replacing `AvoInspector` with `AvoInspectorLite`, or `AvoSchemaParser` with `AvoSchemaParserLite`. No full source files are modified. | Lite File | Copied From | Change | |---|---|---| | `AvoSchemaParserLite.ts` | `AvoSchemaParser.ts` | Remove `encryptValue` import and all encryption logic | | `AvoDeduplicatorLite.ts` | `AvoDeduplicator.ts` | Import `AvoSchemaParserLite` instead of `AvoSchemaParser` | | `AvoNetworkCallsHandlerLite.ts` | `AvoNetworkCallsHandler.ts` | Import `AvoInspectorLite` + `AvoStreamIdLite`; fix relative paths | | `AvoBatcherLite.ts` | `AvoBatcher.ts` | Import `AvoInspectorLite` + `AvoNetworkCallsHandlerLite`; fix relative paths | | `AvoStreamIdLite.ts` | `AvoStreamId.ts` | Import `AvoInspectorLite` instead of `AvoInspector`; fix relative paths | | `AvoInspectorLite.ts` | `AvoInspector.ts` | No eventSpec, no `publicEncryptionKey`, imports all Lite variants | The `src/lite/index.ts` entry re-exports `AvoInspectorLite as AvoInspector` and `AvoInspectorEnv` — identical surface to the full entry point. --- ## Quality Checks (all stories) - Build: `yarn build` — required on every story - Unit tests: `yarn test --roots='<rootDir>/src'` - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` --- ## Stories ### Story 1 — Lite source files: AvoSchemaParserLite + AvoNetworkCallsHandlerLite **Approach:** general-purpose **Goal:** Create the two foundational lite source files. These have no circular dependency on `AvoInspectorLite` and can be verified standalone. **Files created (2):** - `src/lite/AvoSchemaParserLite.ts` — copy of `AvoSchemaParser.ts` with encryption stripped - `src/lite/AvoNetworkCallsHandlerLite.ts` — copy of `AvoNetworkCallsHandler.ts` with `AvoInspectorLite`/`AvoStreamIdLite` import placeholders **Key implementation notes:** - Create the `src/lite/` directory first: `mkdir -p src/lite` (the directory does not exist in the repo) - `AvoSchemaParserLite`: remove `import { encryptValue } from "./AvoEncryption"` entirely; change types import to `from "./AvoNetworkCallsHandlerLite"`; delete `canSendEncryptedValues`, `getEncryptedPropertyValueIfEnabled`; in `extractSchema` remove the `canSendEncryptedValues` variable and the entire `encryptedValue` block in the primitive branch; keep method signature `static async extractSchema(eventProperties, publicEncryptionKey?, env?)` (params accepted but unused) - `AvoNetworkCallsHandlerLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change `import AvoGuid from "./AvoGuid"` to `import AvoGuid from "../AvoGuid"`; change `import { AvoStreamId } from "./AvoStreamId"` to `import { AvoStreamIdLite as AvoStreamId } from "./AvoStreamIdLite"`; change `import type { EventSpecMetadata } from "./eventSpec/AvoEventSpecFetchTypes"` to `import type { EventSpecMetadata } from "../eventSpec/AvoEventSpecFetchTypes"`; add file header comment - Both files start with the LITE COPY header comment documenting sync obligation - At this stage `AvoInspectorLite` and `AvoStreamIdLite` are forward references — the files reference them in imports but they are created in later stories. TypeScript will not complain until a full `tsc` is run; the build is not expected to pass cleanly until Story 3 **Dependencies:** none **Quality checks:** - Build: `yarn build` (expected to fail until Story 3 — note that in the story, only type-check individual files if desired) - Tests: `yarn test --roots='<rootDir>/src'` (non-required — ts-jest will fail to compile `AvoNetworkCallsHandlerLite.ts` due to the forward import of `AvoInspectorLite`, which does not exist until Story 3; existing tests in other directories should still pass if jest is configured to only compile tested files, but this cannot be guaranteed) - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` (non-required — same ts-jest forward-import constraint applies) --- ### Story 2 — Lite source files: AvoStreamIdLite + AvoDeduplicatorLite + AvoBatcherLite **Approach:** general-purpose **Goal:** Create the remaining three lite copy files that form the infrastructure layer. **Files created (3):** - `src/lite/AvoStreamIdLite.ts` - `src/lite/AvoDeduplicatorLite.ts` - `src/lite/AvoBatcherLite.ts` **Key implementation notes:** - `AvoStreamIdLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change `import AvoGuid from "./AvoGuid"` to `import AvoGuid from "../AvoGuid"`; alias `as AvoInspector` means zero body changes; add LITE COPY header - `AvoDeduplicatorLite`: change `import { AvoSchemaParser } from "./AvoSchemaParser"` to `import { AvoSchemaParserLite as AvoSchemaParser } from "./AvoSchemaParserLite"`; change `import { deepEquals } from "./utils"` to `import { deepEquals } from "../utils"`; alias `as AvoSchemaParser` means zero body changes; add LITE COPY header - `AvoBatcherLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change import of `AvoNetworkCallsHandler` types to from `./AvoNetworkCallsHandlerLite`; change `import type { EventSpecMetadata } from "./eventSpec/AvoEventSpecFetchTypes"` to `import type { EventSpecMetadata } from "../eventSpec/AvoEventSpecFetchTypes"`; keep `eventSpecMetadata?` in `AvoBatcherType` interface; add LITE COPY header - Build still not expected to fully pass (AvoInspectorLite not yet created) **Dependencies:** Story 1 **Quality checks:** - Build: `yarn build` (note: will fail until Story 3 completes; run only to verify no regression in existing output) - Tests: `yarn test --roots='<rootDir>/src'` (non-required — ts-jest will fail to compile lite files that forward-import `AvoInspectorLite`, which does not exist until Story 3) - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` (non-required — same ts-jest forward-import constraint applies) --- ### Story 3 — AvoInspectorLite + index + tsconfig.lite.json + package.json **Approach:** general-purpose **Goal:** Create `AvoInspectorLite.ts` and `src/lite/index.ts`, add `tsconfig.lite.json`, and update `package.json` with the exports map and files field. After this story, `yarn build` must pass cleanly. **Files created (3):** - `src/lite/AvoInspectorLite.ts` - `src/lite/index.ts` - `tsconfig.lite.json` **Files modified (2):** - `package.json` — add `"exports"` map, `"files"` field, update `"build"` script, add `"check:lite-size"` script, add `"verify:lite-sync"` script **Key implementation notes for `AvoInspectorLite.ts`:** - Imports: `AvoInspectorEnv` from `"../AvoInspectorEnv"`; `AvoSchemaParserLite` from `"./AvoSchemaParserLite"`; `AvoBatcherLite as AvoBatcher` from `"./AvoBatcherLite"`; `AvoNetworkCallsHandlerLite as AvoNetworkCallsHandler, type EventProperty` from `"./AvoNetworkCallsHandlerLite"`; `AvoStorage` from `"../AvoStorage"`; `AvoDeduplicatorLite as AvoDeduplicator` from `"./AvoDeduplicatorLite"`; `AvoStreamIdLite as AvoStreamId` from `"./AvoStreamIdLite"`; `isValueEmpty` from `"../utils"`; `libVersion = require("../../package.json").version` - Constructor options: `{ apiKey, env, version, appName?, suffix? }` — no `publicEncryptionKey` - Constructor body: identical to full except: no `this.publicEncryptionKey = ...`; no `this.streamId = AvoStreamId.streamId`; no `if (this.streamId) { ... }` eventSpec init block; pass `undefined` as 6th arg to `new AvoNetworkCallsHandlerLite(..., undefined)` - Static fields: `avoStorage`, `_batchSize` (get+set), `_batchFlushSeconds` (get only — no static setter), `_shouldLog` (get+set), `_networkTimeout` (get+set) — exactly mirroring the full class - `trackSchemaFromEvent`: no `fetchAndValidateEvent` call, no `validationResult` branch; always calls `trackSchemaInternal` directly - `_avoFunctionTrackSchemaFromEvent`: mirrors `trackSchemaFromEvent` but passes `fromAvoFunction: true` and forwards `eventId`/`eventHash` to `trackSchemaInternal` - `trackSchema`: identical to full but without `await this.fetchEventSpecIfNeeded(eventName)` line - `extractSchema`: calls `AvoSchemaParserLite.extractSchema(eventProperties)` — no key, no env **`tsconfig.lite.json`:** ```json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist" }, "include": [ "src/lite/**/*", "src/AvoInspectorEnv.ts", "src/AvoStorage.ts", "src/AvoGuid.ts", "src/utils.ts", "src/eventSpec/AvoEventSpecFetchTypes.ts" ] } ``` **`package.json` changes:** - Add `"files": ["dist", "bin"]` - Add `"exports"` map with `.` and `./lite` conditions, each with `types` and `default` - Update `"build"` to: `"tsc --emitDeclarationOnly && tsc --project tsconfig.lite.json --noEmit && webpack --config webpack.config.js"` - Add `"check:lite-size": "node scripts/check-lite-size.js"` - Add `"verify:lite-sync": "bash scripts/verify-lite-sync.sh"` - Update `"prepublishOnly"` to append `&& yarn check:lite-size && yarn verify:lite-sync` so the size gate and drift check are publish-blocking **Acceptance verification after this story:** - `yarn build` passes - `ls dist/lite/index.js dist/lite/index.d.ts` succeeds - `grep -r "AvoEncryption" dist/lite/` returns no matches - `grep -r "AvoEventSpecFetcher" dist/lite/` returns no matches - `grep -r "noble" dist/lite/` returns no matches **Dependencies:** Stories 1, 2 **Quality checks:** - Build: `yarn build` (must pass) - Tests: `yarn test --roots='<rootDir>/src'` - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` --- ### Story 4 — Tests for lite build **Approach:** general-purpose **Goal:** Add Jest tests for `AvoInspectorLite` and `AvoSchemaParserLite` covering all acceptance criteria test categories. **Files created (2):** - `src/__tests__/AvoInspectorLite_test.ts` - `src/__tests__/AvoSchemaParserLite_test.ts` **`AvoInspectorLite_test.ts` test cases:** 1. Constructor accepts `apiKey`, `env`, `version`, `appName`, `suffix` 2. `publicEncryptionKey` causes TypeScript compile error (`@ts-expect-error`) 3. `trackSchemaFromEvent` returns correct schema with no `encryptedPropertyValue` 4. `trackSchemaFromEvent` returns `[]` for deduplicated events 5. `trackSchema` completes without throwing 6. `extractSchema` returns correct schema without encryption 7. Static getters: `batchSize`, `batchFlushSeconds`, `shouldLog`, `networkTimeout` all readable 8. Static setters: `batchSize`, `shouldLog`, `networkTimeout` settable; `batchFlushSeconds` set only via `setBatchFlushSeconds(n)` instance method 9. Import from `src/lite/index.ts` exports `AvoInspector` and `AvoInspectorEnv` 10. Static isolation test: `AvoInspector.batchSize = 99` does NOT change `AvoInspectorLite.batchSize` **`AvoSchemaParserLite_test.ts` test cases:** 1. Correct property types (string, int, float, boolean, null, list, object) 2. Never sets `encryptedPropertyValue` even if `publicEncryptionKey` is passed 3. Nested objects extracted correctly 4. Arrays/lists extracted correctly **Pattern notes:** - Follow existing test patterns from `src/__tests__/Parsing_test.ts` and `src/__tests__/AvoInspectorEventSpec_test.ts` - Mock `XMLHttpRequest` for network calls (same pattern as existing tests) - Use `src/__tests__/constants.ts` `defaultOptions` as the base **Dependencies:** Story 3 **Quality checks:** - Build: `yarn build` - Tests: `yarn test --roots='<rootDir>/src'` (all new tests must pass, existing must still pass) - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` --- ### Story 5 — Drift detection and size verification scripts **Approach:** general-purpose **Goal:** Add the CI scripts that enforce drift detection between lite copies and their full originals, and the automated 7 KB gzip size gate. **Files created (3):** - `scripts/verify-lite-sync.sh` - `scripts/check-lite-size.js` - `scripts/webpack.lite-size.config.js` **Key implementation notes:** `scripts/verify-lite-sync.sh`: - Uses `PAIRS` array of 4 pairs (AvoNetworkCallsHandler, AvoBatcher, AvoStreamId, AvoDeduplicator with their Lite copies) - Add comment above PAIRS explaining `AvoSchemaParserLite` is excluded because it removes entire methods (encryption), not just one import line — requires manual review - Fails if any pair has more than 10 changed lines (unexpected drift) - Script is `chmod +x` executable `scripts/check-lite-size.js`: - Before running webpack, call `fs.mkdirSync(path.resolve(__dirname, '../test-bundle-size/output'), { recursive: true })` to guard against a missing output directory on fresh clones (mirrors the defensive `mkdir -p test-bundle-size/output` in the existing `analyze.sh`) - Runs `npx webpack --config scripts/webpack.lite-size.config.js` - Then `npx terser test-bundle-size/output/bundle-lite.js --compress passes=2,unsafe=true --mangle` writing to `test-bundle-size/output/bundle-lite-terser.js` - Gzips `bundle-lite-terser.js` and fails if > 7168 bytes `scripts/webpack.lite-size.config.js`: - Entry: `./dist/lite/index.js` - Output: `bundle-lite.js` in `test-bundle-size/output/` - Mode: production, no `ts-loader` (compiles from already-built JS), `libraryTarget: "umd"`, `splitChunks: false` **Note:** The `scripts/` directory does not exist in the repo. Create it with `mkdir -p scripts` before writing files. **Dependencies:** Story 3 **Quality checks:** - Build: `yarn build` - Tests: `yarn test --roots='<rootDir>/src'` - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` - Manual: `yarn verify:lite-sync` passes; `yarn check:lite-size` passes (confirms < 7 KB) --- ### Story 6 — Example apps: examples/lite-size-demos/ **Approach:** general-purpose **Goal:** Add three self-contained example apps demonstrating the lite bundle with different toolchains. These serve as documentation and proof that the lite build works universally. **Files created (11):** - `examples/lite-size-demos/terser-only/entry.js` - `examples/lite-size-demos/terser-only/build.sh` - `examples/lite-size-demos/terser-only/README.md` - `examples/lite-size-demos/webpack/entry.js` - `examples/lite-size-demos/webpack/webpack.config.js` - `examples/lite-size-demos/webpack/package.json` — declares `webpack` and `webpack-cli` as devDependencies so the example is self-contained - `examples/lite-size-demos/webpack/README.md` - `examples/lite-size-demos/rollup/entry.js` - `examples/lite-size-demos/rollup/rollup.config.js` - `examples/lite-size-demos/rollup/package.json` — declares `rollup`, `@rollup/plugin-node-resolve`, and `@rollup/plugin-commonjs` as devDependencies so the example is self-contained - `examples/lite-size-demos/rollup/README.md` **Key implementation notes:** - `terser-only/entry.js`: `const { AvoInspector, AvoInspectorEnv } = require("avo-inspector/lite"); ...` — construct and call `trackSchemaFromEvent` - `terser-only/build.sh`: esbuild bundles to `build/bundle.js`, terser minifies to `build/bundle.min.js` - `webpack/entry.js`: ES module import from `"avo-inspector/lite"` - `webpack/webpack.config.js`: production mode, UMD output - `webpack/package.json`: include `webpack` and `webpack-cli` in `devDependencies`; a user running this example needs to `npm install` inside the directory first - `rollup/entry.js`: ES module import from `"avo-inspector/lite"` - `rollup/rollup.config.js`: uses `@rollup/plugin-node-resolve` and `@rollup/plugin-commonjs` - `rollup/package.json`: include `rollup`, `@rollup/plugin-node-resolve`, and `@rollup/plugin-commonjs` in `devDependencies`; a user running this example needs to `npm install` inside the directory first - Each `README.md` shows: exact import, exact build command, expected raw/minified/gzipped sizes (placeholder `~X KB` until built), and grep command proving no encryption code present - Add `"examples/lite-size-demos/"` to the `"files"` array in `package.json` (update Story 3's `package.json` change if needed, or do it here) - **Merge-conflict warning:** Story 6 and Story 3 both modify `package.json`. Although Story 6 depends on Story 3 (so they are logically sequenced), if both stories are executed in parallel by different agents they will produce a merge conflict on `package.json`. Story 6's `package.json` edit (adding `"examples/lite-size-demos/"` to `files`) must be applied only after Story 3's `package.json` changes are fully committed. **Dependencies:** Story 3 **Quality checks:** - Build: `yarn build` - Tests: `yarn test --roots='<rootDir>/src'` - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` --- ### Story 7 — Final verification pass **Approach:** general-purpose **Goal:** Run all acceptance criteria checks end-to-end, fix any issues found, and confirm the complete feature is ready for review. **Files modified (0 expected — fixes only if needed):** - Any file that needs correction based on end-to-end acceptance criteria checks **Verification checklist:** 1. `yarn build` — succeeds, produces `dist/index.js` and `dist/lite/index.js` 2. `tsc --project tsconfig.lite.json --noEmit` — passes (already part of build) 3. `grep -r "AvoEncryption" dist/lite/` — no matches 4. `grep -r "AvoEventSpecFetcher" dist/lite/` — no matches 5. `grep -r "EventSpecCache" dist/lite/` — no matches 6. `grep -r "noble" dist/lite/` — no matches 7. `grep -r "safe-regex" dist/lite/` — no matches 8. `yarn test --roots='<rootDir>/src'` — all tests pass 9. `BROWSER=1 yarn test --roots='<rootDir>/src'` — all tests pass 10. `npm pack --dry-run | grep "dist/lite"` — includes `dist/lite/index.js` and `dist/lite/index.d.ts` 11. `yarn verify:lite-sync` — no drift detected 12. `yarn check:lite-size` — under 7168 bytes gzipped 13. Static isolation test passes (batchSize isolation) 14. All 24 spec acceptance criteria verified 15. AC 21 — `cd examples/lite-size-demos/terser-only && bash build.sh` runs successfully and `build/bundle.min.js` gzipped is under 7 KB 16. AC 22 — `cd examples/lite-size-demos/webpack && npm install && npx webpack` runs successfully and output gzipped is under 7 KB 17. AC 23 — `cd examples/lite-size-demos/rollup && npm install && npx rollup -c` runs successfully and output gzipped is under 7 KB **Dependencies:** Stories 1, 2, 3, 4, 5, 6 **Quality checks:** - Build: `yarn build` - Tests: `yarn test --roots='<rootDir>/src'` - Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` --- ## Story Dependency Graph ``` Story 1 (AvoSchemaParserLite + AvoNetworkCallsHandlerLite) | Story 2 (AvoStreamIdLite + AvoDeduplicatorLite + AvoBatcherLite) | Story 3 (AvoInspectorLite + index + tsconfig.lite.json + package.json) |---------------|---------------| Story 4 (Tests) Story 5 (Scripts) Story 6 (Examples) | | | `---------------+---------------' | Story 7 (Final verification) ``` Stories 4, 5, and 6 are independent of each other after Story 3 and can be worked in parallel.