JavaScript SDK: All unit tests passing (268/268), cross-SDK tests (138/138)#50
JavaScript SDK: All unit tests passing (268/268), cross-SDK tests (138/138)#50
Conversation
…ssing) - Use TypeScript `type` keyword for type-only imports across all modules - Replace forEach with for...of loops per code style conventions - Extract magic numbers into named constants (DEFAULT_RETRIES, DEFAULT_TIMEOUT_MS, RETRY_DELAY_MS) - Add input validation for attribute names in Context - Simplify Context.ready() promise handling - Add regex pattern/text length limits in MatchOperator for safety - Improve error messages in AudienceMatcher and MatchOperator - Modernize stringToUint8Array to use TextEncoder - Use URLSearchParams for query string building in Client - Extract SDK._extractClientOptions into a static method - Add Absmartly alias export in index.ts - Add 226 lines of new context test coverage (custom fields, attributes, overrides, audience matching)
WalkthroughThis pull request introduces comprehensive changes across documentation, test coverage, and source code. The README undergoes a substantial overhaul with branding adjustments, expanded usage examples for Node.js and browser environments, and reorganised sections for context creation, unit management, and event logging. The codebase receives type-only import conversions throughout, input validation enhancements on public-facing methods, and new public accessors ( Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.41.0)src/__tests__/context.test.jsThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fix eq operator to handle numeric string comparisons and in operator to properly check string containment and collection membership.
Exposes the SDK instance and context options through public API so React and Angular SDKs no longer need to access private _sdk/_opts fields.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
src/fetch.ts (1)
34-37: Consider throwing an error when no fetch implementation is available.Returning
undefinedwhen no fetch implementation is found could lead to confusing runtime errors whenexportedis invoked. A descriptive error would aid debugging.💡 Suggested improvement
- return undefined; + throw new Error( + "No fetch implementation available. Ensure you are running in a supported environment (browser, Node.js, or Web Worker)." + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fetch.ts` around lines 34 - 37, getFetchImplementation currently returns undefined when no fetch implementation is found, which causes unclear runtime failures when the module-level exported variable exported is used; change getFetchImplementation so that instead of returning undefined it throws a clear, descriptive Error (e.g., "No fetch implementation available: please provide global fetch or a polyfill") and ensure the module still assigns const exported = getFetchImplementation(); so callers fail fast with the descriptive exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 131-141: Replace the non-standard script tag attribute
type="javascript" with a valid attribute (either type="text/javascript") or
remove the type attribute entirely so the browser treats it as JavaScript;
update the <script> tag that contains the request object and the call to
sdk.createContextWith(request, {{ serverSideContext.data() }}) to use the
corrected type or no type so the snippet is valid.
- Around line 51-54: The fenced code block containing "Browser --> Your Server
(with API key) --> ABsmartly API" is missing a language specifier; update that
fenced block in README.md to use a language token such as ```text or
```plaintext so the block becomes ```text ... ``` and satisfies linting and
improves rendering consistency.
In `@src/client.ts`:
- Line 268: The retry wrapper call uses the raw instance option
this._opts.timeout instead of the resolved per-request timeout
(options.timeout), causing incorrect retry/timeout accounting; update the call
to tryWith in the function that currently passes this._opts.timeout to instead
pass the already-resolved timeout (e.g., options.timeout) with the same fallback
(options.timeout ?? DEFAULT_TIMEOUT_MS) while keeping the retries fallback
(this._opts.retries ?? DEFAULT_RETRIES) unchanged.
In `@src/context.ts`:
- Around line 835-845: The scheduled publish/refresh timeout handlers call
this._logError in their outer catch, which duplicates errors already logged by
this._flush and this._refresh; remove the outer this._logError calls inside the
setTimeout handlers (the handlers that call this._flush and this._refresh via
the this._publishTimeout and the similar scheduled block at 1038-1048) so that
only the inner _flush/_refresh error paths log the error. Ensure the timeout
handlers still swallow/recover the error (no rethrow) after removing the outer
log to preserve behavior.
In `@src/fetch.ts`:
- Around line 19-32: The current runtime check uses a bare identifier "global"
which can throw ReferenceError in strict browser environments; update the check
to safely detect the environment (e.g., use typeof global !== "undefined" or
globalThis) and prefer globalThis.fetch when available, and only fall back to
importing "node-fetch" if no fetch exists; update the branch that currently
returns global.fetch.bind(global) and the fallback that imports "node-fetch"
(the anonymous function handling url/opts and the import("node-fetch") logic) to
use the safe existence check so no ReferenceError occurs.
In `@src/jsonexpr/operators/match.ts`:
- Around line 13-23: Before compiling untrusted regex patterns, validate them
with a ReDoS-safe checker: import safe-regex2 (e.g. safeRegex) and, after the
text length check and before new RegExp(pattern), call if (!safeRegex(pattern))
{ console.error("Unsafe regex pattern rejected"); return null; } — update the
code around the RegExp compilation and the variables pattern, text,
MAX_PATTERN_LENGTH, MAX_TEXT_LENGTH, and compiled to perform this check so
catastrophic-backtracking patterns are rejected prior to compilation.
---
Nitpick comments:
In `@src/fetch.ts`:
- Around line 34-37: getFetchImplementation currently returns undefined when no
fetch implementation is found, which causes unclear runtime failures when the
module-level exported variable exported is used; change getFetchImplementation
so that instead of returning undefined it throws a clear, descriptive Error
(e.g., "No fetch implementation available: please provide global fetch or a
polyfill") and ensure the module still assigns const exported =
getFetchImplementation(); so callers fail fast with the descriptive exception.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
.gitignoreREADME.mdsrc/__tests__/context.test.jssrc/__tests__/jsonexpr/operators/eq.test.jssrc/__tests__/jsonexpr/operators/in.test.jssrc/abort-controller-shim.tssrc/client.tssrc/context.tssrc/fetch.tssrc/index.tssrc/jsonexpr/operators/eq.tssrc/jsonexpr/operators/in.tssrc/jsonexpr/operators/match.tssrc/matcher.tssrc/provider.tssrc/publisher.tssrc/sdk.tssrc/utils.ts
| ``` | ||
| Browser --> Your Server (with API key) --> ABsmartly API | ||
| session token only | ||
| ``` |
There was a problem hiding this comment.
Add a language specifier to the fenced code block.
The architecture diagram code block lacks a language specifier. While this is plain text, adding text or plaintext satisfies linting rules and improves rendering consistency.
📝 Proposed fix
-```
+```text
Browser --> Your Server (with API key) --> ABsmartly API
session token only</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 51-51: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 51 - 54, The fenced code block containing "Browser
--> Your Server (with API key) --> ABsmartly API" is missing a language
specifier; update that fenced block in README.md to use a language token such as
```text or ```plaintext so the block becomes ```text ... ``` and satisfies
linting and improves rendering consistency.
| <head> | ||
| <script type="javascript"> | ||
| const request = { | ||
| units: { | ||
| session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", | ||
| }, | ||
| }; | ||
|
|
||
| const context = sdk.createContextWith(request, {{ serverSideContext.data() }}); | ||
| </script> | ||
| </head> |
There was a problem hiding this comment.
Fix invalid type attribute in script tag.
The type="javascript" attribute is non-standard. Use type="text/javascript" or omit the attribute entirely (modern browsers default to JavaScript).
📝 Proposed fix
- <script type="javascript">
+ <script type="text/javascript">Or simply:
- <script type="javascript">
+ <script>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <head> | |
| <script type="javascript"> | |
| const request = { | |
| units: { | |
| session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", | |
| }, | |
| }; | |
| const context = sdk.createContextWith(request, {{ serverSideContext.data() }}); | |
| </script> | |
| </head> | |
| <head> | |
| <script> | |
| const request = { | |
| units: { | |
| session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", | |
| }, | |
| }; | |
| const context = sdk.createContextWith(request, {{ serverSideContext.data() }}); | |
| </script> | |
| </head> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 131 - 141, Replace the non-standard script tag
attribute type="javascript" with a valid attribute (either
type="text/javascript") or remove the type attribute entirely so the browser
treats it as JavaScript; update the <script> tag that contains the request
object and the call to sdk.createContextWith(request, {{
serverSideContext.data() }}) to use the corrected type or no type so the snippet
is valid.
| }; | ||
|
|
||
| return tryWith(this._opts.retries ?? 5, this._opts.timeout ?? 3000) | ||
| return tryWith(this._opts.retries ?? DEFAULT_RETRIES, this._opts.timeout ?? DEFAULT_TIMEOUT_MS) |
There was a problem hiding this comment.
Per-request timeout is not honoured in retry accounting.
Line 268 passes this._opts.timeout into tryWith, but Line 252 already resolves the effective timeout (options.timeout fallback included). This can produce wrong retry/timeout behaviour for per-request overrides.
💡 Proposed fix
- return tryWith(this._opts.retries ?? DEFAULT_RETRIES, this._opts.timeout ?? DEFAULT_TIMEOUT_MS)
+ return tryWith(this._opts.retries ?? DEFAULT_RETRIES, timeout)
.then((value: string) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return tryWith(this._opts.retries ?? DEFAULT_RETRIES, this._opts.timeout ?? DEFAULT_TIMEOUT_MS) | |
| return tryWith(this._opts.retries ?? DEFAULT_RETRIES, timeout) | |
| .then((value: string) => { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/client.ts` at line 268, The retry wrapper call uses the raw instance
option this._opts.timeout instead of the resolved per-request timeout
(options.timeout), causing incorrect retry/timeout accounting; update the call
to tryWith in the function that currently passes this._opts.timeout to instead
pass the already-resolved timeout (e.g., options.timeout) with the same fallback
(options.timeout ?? DEFAULT_TIMEOUT_MS) while keeping the retries fallback
(this._opts.retries ?? DEFAULT_RETRIES) unchanged.
| this._publishTimeout = setTimeout(async () => { | ||
| try { | ||
| await new Promise<void>((resolve, reject) => { | ||
| this._flush((error?: Error) => { | ||
| if (error) reject(error); | ||
| else resolve(); | ||
| }); | ||
| }); | ||
| } catch (error) { | ||
| this._logError(error as Error); | ||
| } |
There was a problem hiding this comment.
Scheduled publish/refresh failures are logged twice.
At Line 844 and Line 1047, the outer catch logs again, but _flush (Lines 917-919) and _refresh (Lines 949-950) already log the same error. This duplicates "error" events for a single failure.
💡 Proposed fix
- } catch (error) {
- this._logError(error as Error);
+ } catch {
+ // _flush already logs publish errors.
}
@@
- } catch (error) {
- this._logError(error as Error);
+ } catch {
+ // _refresh already logs refresh errors.
}Also applies to: 1038-1048
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.ts` around lines 835 - 845, The scheduled publish/refresh timeout
handlers call this._logError in their outer catch, which duplicates errors
already logged by this._flush and this._refresh; remove the outer this._logError
calls inside the setTimeout handlers (the handlers that call this._flush and
this._refresh via the this._publishTimeout and the similar scheduled block at
1038-1048) so that only the inner _flush/_refresh error paths log the error.
Ensure the timeout handlers still swallow/recover the error (no rethrow) after
removing the outer log to preserve behavior.
| if (global) { | ||
| if (global.fetch) { | ||
| return global.fetch.bind(global); | ||
| } | ||
| return function (url: string, opts: Record<string, unknown>) { | ||
| return new Promise((resolve, reject) => { | ||
| import("node-fetch") | ||
| .then((fetchNode) => { | ||
| fetchNode.default(url.replace(/^\/\//g, "https://"), opts).then(resolve).catch(reject); | ||
| }) | ||
| .catch(reject); | ||
| }); | ||
| }; | ||
| } |
There was a problem hiding this comment.
Potential ReferenceError when accessing global in strict browser environments.
The check if (global) may throw a ReferenceError in browser environments where global is not defined. Consider using a safer check:
🛡️ Proposed fix for safer global check
- if (global) {
- if (global.fetch) {
- return global.fetch.bind(global);
+ if (typeof global !== "undefined") {
+ if (global.fetch) {
+ return global.fetch.bind(global);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (global) { | |
| if (global.fetch) { | |
| return global.fetch.bind(global); | |
| } | |
| return function (url: string, opts: Record<string, unknown>) { | |
| return new Promise((resolve, reject) => { | |
| import("node-fetch") | |
| .then((fetchNode) => { | |
| fetchNode.default(url.replace(/^\/\//g, "https://"), opts).then(resolve).catch(reject); | |
| }) | |
| .catch(reject); | |
| }); | |
| }; | |
| } | |
| if (typeof global !== "undefined") { | |
| if (global.fetch) { | |
| return global.fetch.bind(global); | |
| } | |
| return function (url: string, opts: Record<string, unknown>) { | |
| return new Promise((resolve, reject) => { | |
| import("node-fetch") | |
| .then((fetchNode) => { | |
| fetchNode.default(url.replace(/^\/\//g, "https://"), opts).then(resolve).catch(reject); | |
| }) | |
| .catch(reject); | |
| }); | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/fetch.ts` around lines 19 - 32, The current runtime check uses a bare
identifier "global" which can throw ReferenceError in strict browser
environments; update the check to safely detect the environment (e.g., use
typeof global !== "undefined" or globalThis) and prefer globalThis.fetch when
available, and only fall back to importing "node-fetch" if no fetch exists;
update the branch that currently returns global.fetch.bind(global) and the
fallback that imports "node-fetch" (the anonymous function handling url/opts and
the import("node-fetch") logic) to use the safe existence check so no
ReferenceError occurs.
| if (pattern.length > MAX_PATTERN_LENGTH) { | ||
| console.error(`Regex pattern too long: ${pattern.length} > ${MAX_PATTERN_LENGTH}`); | ||
| return null; | ||
| } | ||
| if (text.length > MAX_TEXT_LENGTH) { | ||
| console.error(`Text too long for regex matching: ${text.length} > ${MAX_TEXT_LENGTH}`); | ||
| return null; | ||
| } | ||
| try { | ||
| const compiled = new RegExp(pattern); | ||
| return compiled.test(text); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -name "match.ts" -type f 2>/dev/null | head -20Repository: absmartly/javascript-sdk
Length of output: 50
🏁 Script executed:
git ls-files | grep -E "match\.ts|package\.json"Repository: absmartly/javascript-sdk
Length of output: 112
🏁 Script executed:
cat -n src/jsonexpr/operators/match.ts | head -50Repository: absmartly/javascript-sdk
Length of output: 1251
🏁 Script executed:
cat package.json | head -100Repository: absmartly/javascript-sdk
Length of output: 3016
🏁 Script executed:
grep -i "safe-regex\|redos\|security" package.jsonRepository: absmartly/javascript-sdk
Length of output: 50
🏁 Script executed:
git log --oneline src/jsonexpr/operators/match.ts | head -10Repository: absmartly/javascript-sdk
Length of output: 137
🌐 Web query:
safe-regex2 npm package maintained
💡 Result:
Yes—safe-regex2 is still maintained (it’s in the fastify/safe-regex2 repo). The latest release is v5.0.0 (Mar 8, 2025) [1], and the repository has continued to receive commits after that (commit activity shown through Oct 17, 2025) [2]. Third-party health trackers also rate its maintenance as “Sustainable” and report recent commits/releases within the last year (relative to their scan dates) [3].
Sources:
[1] fastify/safe-regex2 GitHub releases (v5.0.0) (github.com)
[2] fastify/safe-regex2 commit history (github.com)
[3] Snyk package health for safe-regex2 (security.snyk.io)
Citations:
- 1: https://github.com/fastify/safe-regex2
- 2: https://github.com/fastify/safe-regex2/commits/main/
- 3: https://security.snyk.io/package/npm/safe-regex2?utm_source=openai
🏁 Script executed:
grep -r "safeRegex\|safe-regex\|ReDoS" src/ --include="*.ts" --include="*.js"Repository: absmartly/javascript-sdk
Length of output: 50
🏁 Script executed:
grep -r "RegExp\|new RegExp" src/jsonexpr/ --include="*.ts" | head -20Repository: absmartly/javascript-sdk
Length of output: 142
Add ReDoS protection before regex compilation in dynamic pattern matching.
Length caps alone do not prevent ReDoS attacks. Patterns like (a+)+ or (a|a)+ cause catastrophic backtracking despite being short enough to pass the 1000-character limit. Since this SDK processes untrusted user-supplied regex patterns, add pattern validation using safe-regex2 before compilation:
import safeRegex from "safe-regex2";
// After text length check, before RegExp compilation:
if (!safeRegex(pattern)) {
console.error("Unsafe regex pattern rejected");
return null;
}The try-catch block catches syntax errors only; it does not prevent execution hangs caused by catastrophic backtracking.
🧰 Tools
🪛 ast-grep (0.41.0)
[warning] 21-21: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(pattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/jsonexpr/operators/match.ts` around lines 13 - 23, Before compiling
untrusted regex patterns, validate them with a ReDoS-safe checker: import
safe-regex2 (e.g. safeRegex) and, after the text length check and before new
RegExp(pattern), call if (!safeRegex(pattern)) { console.error("Unsafe regex
pattern rejected"); return null; } — update the code around the RegExp
compilation and the variables pattern, text, MAX_PATTERN_LENGTH,
MAX_TEXT_LENGTH, and compiled to perform this check so catastrophic-backtracking
patterns are rejected prior to compilation.
Summary
typekeyword for type-only importsforEachwithfor...ofloops for consistency with code styleDEFAULT_RETRIES,DEFAULT_TIMEOUT_MS,RETRY_DELAY_MS)Context.ready()promise handlingMatchOperatorstringToUint8Arrayto useTextEncoderURLSearchParamsfor query string building inClientSDKconstructor to extract client option parsing into static methodAbsmartlyalias export inindex.tsTest plan
npm test, exit code 0)forEach->for...ofrefactoringTextEncoderavailability in target environmentsSummary by CodeRabbit
New Features
Documentation
Tests
Chores