mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(feedback): gate onboarding survey on first processing, add prompt lifecycle events (#615)
Defers the onboarding usage survey to the instance's first successful processing (the worker writes a one-time onboarding.firstProcessedAt marker and the overlay gates on it), so it reaches engaged users instead of first-landing visitors. Replaces the two questions telemetry already answers (modality preference from tool_used, install method from instance_started) with what it can't infer: prior tool, self-host motivation, and discovery source. Adds feedback_prompt_shown and feedback_prompt_dismissed on all five feedback surfaces (usage survey, per-job prompt, admin install card, global nav dialog, search-miss) so skip and completion rates are measurable, not just submissions. New survey strings translated into all 20 non-English locales.
This commit is contained in:
@@ -306,6 +306,9 @@ describe("captureFeedback", () => {
|
||||
survey_id: "onboarding-usage-v1",
|
||||
contact_ok: false,
|
||||
usage_type: "personal",
|
||||
prior_tool: "command_line",
|
||||
selfhost_motivation: "privacy_control",
|
||||
discovery_source: "github",
|
||||
},
|
||||
"distinct-onboarding",
|
||||
);
|
||||
@@ -314,6 +317,12 @@ describe("captureFeedback", () => {
|
||||
expect.objectContaining({
|
||||
distinctId: "distinct-onboarding",
|
||||
event: "onboarding_survey_submitted",
|
||||
properties: expect.objectContaining({
|
||||
usage_type: "personal",
|
||||
prior_tool: "command_line",
|
||||
selfhost_motivation: "privacy_control",
|
||||
discovery_source: "github",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { db, schema } from "../../../apps/api/src/db/index.js";
|
||||
import {
|
||||
getSettingNumber,
|
||||
getSettingString,
|
||||
setSettingIfAbsent,
|
||||
upsertSetting,
|
||||
} from "../../../apps/api/src/lib/settings-helpers.js";
|
||||
|
||||
@@ -117,3 +118,28 @@ describe("getSettingString", () => {
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSettingIfAbsent", () => {
|
||||
it("writes the value when the key is absent", async () => {
|
||||
const key = uniqueKey();
|
||||
keysToClean.push(key);
|
||||
|
||||
await setSettingIfAbsent(key, "first");
|
||||
|
||||
const rows = await db.select().from(schema.settings).where(eq(schema.settings.key, key));
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].value).toBe("first");
|
||||
});
|
||||
|
||||
it("keeps the first value and ignores later writes (first-write-wins)", async () => {
|
||||
const key = uniqueKey();
|
||||
keysToClean.push(key);
|
||||
|
||||
await setSettingIfAbsent(key, "first");
|
||||
await setSettingIfAbsent(key, "second");
|
||||
|
||||
const rows = await db.select().from(schema.settings).where(eq(schema.settings.key, key));
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].value).toBe("first");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user