Skip to content

Commit 36aa96e

Browse files
itsjaviclaude
authored andcommitted
feat: trace setEmbeddedData at debug level, keys only [ENG-2472]
The bag is otherwise invisible — memory-only, no getter — so a developer wiring up setEmbeddedData gets no confirmation until a survey happens to display. Mirrors the js-core trace from formbricks/formbricks#9091: the keys set and removed, what the bag now holds, and the sentence that pre-empts the next question ("keys land on a response only if the survey declares them"). Values never reach a log line: the documented use of this bag includes hashed identity fields. Debug level, which this SDK gates on __DEV__, so a release build never prints it. Five tests, one of them the no-values assertion — mutation-checked by making the trace print values, which turns exactly three of them red.
1 parent 941d20b commit 36aa96e

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

‎packages/react-native/src/lib/survey/embedded-data.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,14 @@ export class EmbeddedDataStore {
6060
return;
6161
}
6262

63+
const set: string[] = [];
64+
const removed: string[] = [];
65+
6366
for (const [key, value] of Object.entries(data)) {
6467
if (value === undefined) continue;
6568
if (value === null) {
6669
this.data.delete(key);
70+
removed.push(key);
6771
continue;
6872
}
6973
// Refused rather than stored: `toISOString()` throws a RangeError on an invalid Date, and
@@ -78,7 +82,17 @@ export class EmbeddedDataStore {
7882
continue;
7983
}
8084
this.data.set(key, value);
85+
set.push(key);
8186
}
87+
88+
// A success trace, because the bag is otherwise invisible: it lives in memory (nothing in async
89+
// storage to inspect) and the API has no getter, so without this line a developer wiring up
90+
// `setEmbeddedData` gets zero confirmation until a survey happens to display. Debug level, which
91+
// this SDK gates on `__DEV__`, so a release build never prints it. Keys only, never values — the
92+
// documented use of this bag includes hashed identity fields.
93+
Logger.getInstance().debug(
94+
`setEmbeddedData: set [${set.join(", ")}]${removed.length > 0 ? `, removed [${removed.join(", ")}]` : ""} — the bag now holds [${[...this.data.keys()].join(", ")}]. Keys land on a response only if the survey declares them as ingested Embedded Data fields.`,
95+
);
8296
}
8397

8498
/**
@@ -90,7 +104,11 @@ export class EmbeddedDataStore {
90104
*/
91105
public clearEmbeddedData(...args: [] | [key: string]): void {
92106
if (args.length === 0) {
107+
const clearedCount = this.data.size;
93108
this.data.clear();
109+
Logger.getInstance().debug(
110+
`clearEmbeddedData: cleared the whole bag (${String(clearedCount)} keys)`,
111+
);
94112
return;
95113
}
96114

@@ -103,6 +121,9 @@ export class EmbeddedDataStore {
103121
}
104122

105123
this.data.delete(key);
124+
Logger.getInstance().debug(
125+
`clearEmbeddedData: removed "${key}" — the bag now holds [${[...this.data.keys()].join(", ")}]`,
126+
);
106127
}
107128

108129
/**

‎packages/react-native/src/lib/survey/tests/embedded-data.test.ts‎

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { beforeEach, describe, expect, test, vi } from "vitest";
22
import { EmbeddedDataStore } from "@/lib/survey/embedded-data";
33

4-
// The guards log through Logger; mocked so refused inputs don't spray the test output.
4+
const { mockLogger } = vi.hoisted(() => ({
5+
mockLogger: { error: vi.fn(), debug: vi.fn() },
6+
}));
7+
8+
// The guards log errors and the success trace logs at debug; mocked so refused inputs don't spray
9+
// the test output, and a stable instance lets the trace tests below assert on what was written.
510
vi.mock("@/lib/common/logger", () => ({
6-
Logger: { getInstance: vi.fn(() => ({ error: vi.fn(), debug: vi.fn() })) },
11+
Logger: { getInstance: vi.fn(() => mockLogger) },
712
}));
813

914
type TSetInput = Parameters<EmbeddedDataStore["setEmbeddedData"]>[0];
@@ -166,3 +171,67 @@ describe("input guards (never fatal)", () => {
166171
expect(store.getSnapshot()).toEqual({ plan: "pro", screen: "product" });
167172
});
168173
});
174+
175+
describe("the debug success trace — the bag's only success feedback", () => {
176+
let store: EmbeddedDataStore;
177+
178+
beforeEach(() => {
179+
store = EmbeddedDataStore.getInstance();
180+
store.clearEmbeddedData();
181+
mockLogger.debug.mockClear();
182+
});
183+
184+
test("a successful set logs the keys it set and what the bag now holds — keys only, never values", () => {
185+
store.setEmbeddedData({ plan: "pro", hashed_email: "s3cret-hash" });
186+
187+
expect(mockLogger.debug).toHaveBeenCalledTimes(1);
188+
const message = mockLogger.debug.mock.calls[0][0] as string;
189+
expect(message).toContain("set [plan, hashed_email]");
190+
expect(message).toContain("the bag now holds [plan, hashed_email]");
191+
// The bag's documented use includes hashed identity fields; values must never reach a log line.
192+
expect(message).not.toContain("pro");
193+
expect(message).not.toContain("s3cret-hash");
194+
});
195+
196+
test("a null removal shows up in the trace as removed, not set", () => {
197+
store.setEmbeddedData({ plan: "pro" });
198+
mockLogger.debug.mockClear();
199+
200+
store.setEmbeddedData({ plan: null, screen: "product" });
201+
202+
const message = mockLogger.debug.mock.calls[0][0] as string;
203+
expect(message).toContain("set [screen]");
204+
expect(message).toContain("removed [plan]");
205+
expect(message).toContain("the bag now holds [screen]");
206+
});
207+
208+
test("a skipped invalid Date appears in neither list, and its value never reaches the log", () => {
209+
store.setEmbeddedData({ plan: "pro", signedUpAt: new Date("nope") });
210+
211+
const message = mockLogger.debug.mock.calls[0][0] as string;
212+
expect(message).toContain("set [plan]");
213+
expect(message).not.toContain("signedUpAt");
214+
});
215+
216+
test("clearEmbeddedData traces both forms", () => {
217+
store.setEmbeddedData({ plan: "pro", screen: "product" });
218+
mockLogger.debug.mockClear();
219+
220+
store.clearEmbeddedData("plan");
221+
expect(mockLogger.debug.mock.calls[0][0]).toContain('removed "plan"');
222+
223+
store.clearEmbeddedData();
224+
expect(mockLogger.debug.mock.calls[1][0]).toContain(
225+
"cleared the whole bag (1 keys)",
226+
);
227+
});
228+
229+
test("a refused input logs an error and no success trace", () => {
230+
mockLogger.error.mockClear();
231+
232+
store.setEmbeddedData(null as unknown as TSetInput);
233+
234+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
235+
expect(mockLogger.debug).not.toHaveBeenCalled();
236+
});
237+
});

0 commit comments

Comments
 (0)