Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
439623d
fix: code quality, type safety, error handling, and test coverage (26…
joalves Apr 30, 2026
64eaaba
fix: address PR review comments
joalves Apr 30, 2026
b1b62cd
fix: drop async setTimeout/setInterval to avoid regenerator-runtime
joalves Apr 30, 2026
5bd48d8
fix(jsonexpr): eq(null, null) returns null (canonical null-operand ha…
joalves Jun 16, 2026
627be80
fix: audienceMismatch stays false for null audience eval; override al…
joalves Jun 16, 2026
f3b298a
revert: drop MATCH ReDoS hardening from this branch
joalves Jun 17, 2026
1dfea88
fix: correct SDK alias casing to ABsmartly (brand convention)
joalves Jun 17, 2026
9fa5123
test: add canonical astral/multibyte hashUnit regression test
joalves Jun 17, 2026
2320d54
docs: remove client-side security warning; restore Publishing Pending…
joalves Jun 17, 2026
d096e05
feat!: release v2.0.0 — address PR #50 review feedback
joalves Jun 17, 2026
2f83082
test: use rest-destructure to drop agent in client option test
joalves Jun 17, 2026
7f5d191
fix: ignore js/ and types/ build output in prettier
joalves Jun 24, 2026
7005381
test: add hermetic local-HTTP integration test for real fetch/publish
joalves Jun 25, 2026
2778856
fix: address second-round P2 review feedback (context race, client ti…
joalves Sep 9, 2026
576dd3b
fix: address third-round P2 review feedback (IE10-safe promise chain,…
joalves Sep 9, 2026
2777c8b
docs, test: address remaining review feedback from calthejuggler
joalves Sep 9, 2026
77b5821
fix: protect finalize()/_flush() from throwing custom eventLogger; fi…
joalves Sep 9, 2026
e6db825
fix: isolate observer failures on the failed-init discard path in _fl…
joalves Sep 9, 2026
1bdcf9e
fix: keep ready() always resolving true; restore failed batches in ch…
joalves Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ src/js/*
!src/js/__tests__
types
js
.claude/worktrees
.claude/
.DS_Store
src/version.ts
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ node_modules
coverage
dist
es
js
lib
types
package-lock.json
485 changes: 355 additions & 130 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@absmartly/javascript-sdk",
"version": "1.14.0-beta.1",
"version": "2.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's double check the bump with Márcio - although I think the UID change needs to be major

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version is already at 2.0.0 (major), which matches your instinct that the UID hashing change needs a major bump — it's a breaking change per semver since it can change variant assignment for unit IDs containing astral characters. Leaving this thread open since it explicitly needs your/Márcio's sign-off, not something for me to resolve unilaterally.

"description": "A/B Smartly Javascript SDK",
"homepage": "https://github.com/absmartly/javascript-sdk#README.md",
"bugs": "https://github.com/absmartly/javascript-sdk/issues",
Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/abort-controller-shim.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,45 @@ describe("AbortController", () => {

expect(aborter[Symbol.toStringTag]).toEqual("AbortController");
});

describe("signal.reason", () => {
it("should set default reason on abort()", () => {
const controller = new AbortController();
controller.abort();
expect(controller.signal.aborted).toBe(true);
expect(controller.signal.reason).toBeInstanceOf(Error);
expect(controller.signal.reason.message).toBe("The operation was aborted.");
});

it("should set custom reason on abort(reason)", () => {
const controller = new AbortController();
const customReason = new Error("custom abort");
controller.abort(customReason);
expect(controller.signal.reason).toBe(customReason);
});

it("should have undefined reason before abort", () => {
const controller = new AbortController();
expect(controller.signal.reason).toBeUndefined();
});
});

describe("dispatchEvent onabort handling", () => {
it("should call onabort handler on abort dispatch", () => {
const signal = new AbortSignal();
const handler = jest.fn();
signal.onabort = handler;
signal.dispatchEvent({ type: "abort" });
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ type: "abort" });
});

it("should not call onabort for non-abort events", () => {
const signal = new AbortSignal();
const handler = jest.fn();
signal.onabort = handler;
signal.dispatchEvent({ type: "other" });
expect(handler).not.toHaveBeenCalled();
});
});
});
60 changes: 60 additions & 0 deletions src/__tests__/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,29 @@
});
});

it("request() should not throw on unmatched surrogates in query parameters", () => {
fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse));

const client = new Client(clientOptions);

// A lone UTF-16 surrogate is accepted JavaScript string content but is not
// valid UTF-8; unlike URLSearchParams (which substitutes U+FFFD),
// encodeURIComponent() throws URIError on it directly, so it must be
// normalized to well-formed UTF-16 first.
return client
.request({
method: "GET",
path: "/context",
query: { application: "\uD800" },
})
.then((response) => {
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenLastCalledWith(`${endpoint}/context?application=%EF%BF%BD`, expect.any(Object));

expect(response).toEqual(defaultMockResponse);
});
});

it("request() should omit query parameters if dict empty", (done) => {
fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse));

Expand Down Expand Up @@ -1121,7 +1144,7 @@
});

it("getAgent() should return default agent when not specified", () => {
const { agent: _, ...optionsWithoutAgent } = clientOptions;

Check warning on line 1147 in src/__tests__/client.test.js

View workflow job for this annotation

GitHub Actions / build

'_' is assigned a value but never used
const client = new Client(optionsWithoutAgent);
expect(client.getAgent()).toEqual("javascript-client");
});
Expand Down Expand Up @@ -1188,4 +1211,41 @@
done();
});
});

describe("timeout option", () => {
it("should accept an explicit timeout of 0 (nullish coalescing, not falsy)", () => {
const client = new Client({
endpoint,
agent,
environment,
apiKey,
application,
timeout: 0,
});

expect(client).toBeInstanceOf(Client);
});

it("should still retry a failing-then-succeeding request when timeout is 0 (no deadline)", (done) => {
fetch
.mockResolvedValueOnce(responseMock(500, "server error", "server error text"))
.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse));

const client = new Client(Object.assign({}, clientOptions, { timeout: 0, retries: 5 }));

client
.request({
method: "GET",
path: "/context",
})
.then((response) => {
expect(fetch).toHaveBeenCalledTimes(2);
expect(response).toEqual(defaultMockResponse);

done();
});

advanceFakeTimers();
});
});
});
59 changes: 59 additions & 0 deletions src/__tests__/constructor-options.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import SDK from "../sdk";
import Client from "../client";

describe("SDK and Client constructor option merging", () => {
it("should extract client options from SDK options and pass them to the real Client", () => {
const sdk = new SDK({
agent: "test-agent",
apiKey: "key",
application: "app",
endpoint: "http://localhost",
environment: "test",
timeout: 5000,
});

const client = sdk.getClient();
expect(client).toBeInstanceOf(Client);
expect(client.getAgent()).toBe("test-agent");
expect(client.getEnvironment()).toBe("test");
expect(client.getApplication()).toEqual({ name: "app", version: 0 });
});

it("should accept the SDK application option as an object", () => {
const sdk = new SDK({
agent: "test",
apiKey: "key",
application: { name: "myapp", version: "1.2.3" },
endpoint: "http://localhost",
environment: "prod",
});

expect(sdk.getClient().getApplication()).toEqual({ name: "myapp", version: "1.2.3" });
});

it("should merge provided Client options with the defaults", () => {
const client = new Client({
endpoint: "http://test",
agent: "custom-agent",
environment: "prod",
apiKey: "key123",
application: "myapp",
timeout: 10000,
});

expect(client.getAgent()).toBe("custom-agent");
expect(client.getEnvironment()).toBe("prod");
expect(client.getApplication()).toEqual({ name: "myapp", version: 0 });
});

it("should fall back to the default agent when it is omitted", () => {
const client = new Client({
endpoint: "http://test",
environment: "prod",
apiKey: "key123",
application: "myapp",
});

expect(client.getAgent()).toBe("javascript-client");
});
});
Loading