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:
@@ -31,6 +31,7 @@ import {
|
||||
getBundleForTool,
|
||||
getOptionalBundleForTool,
|
||||
isToolInputError,
|
||||
ONBOARDING_FIRST_PROCESSED_KEY,
|
||||
type PipelineExecutedProperties,
|
||||
TOOLS,
|
||||
} from "@snapotter/shared";
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
} from "../lib/object-storage.js";
|
||||
import { OCR_MAX_ENCODED_INPUT_BYTES } from "../lib/ocr-limits.js";
|
||||
import { SCRUB_PDF_PRODUCER_TOOLS, scrubPdfProducer } from "../lib/pdf-producer.js";
|
||||
import { setSettingIfAbsent } from "../lib/settings-helpers.js";
|
||||
import { timeoutMessage } from "../lib/timeout.js";
|
||||
import { InputValidationError } from "../modality/contract.js";
|
||||
import {
|
||||
@@ -493,6 +495,13 @@ async function processToolJob(job: Job<ToolJobData>): Promise<ToolJobResult> {
|
||||
},
|
||||
data.analyticsDistinctId,
|
||||
);
|
||||
// Mark the instance's first successful processing so the onboarding
|
||||
// survey only appears once the admin has produced a real result
|
||||
// (shouldShowUsageSurvey gate in web feedback.ts). First-write-wins, so
|
||||
// the timestamp reflects the genuine first job and later jobs no-op.
|
||||
void setSettingIfAbsent(ONBOARDING_FIRST_PROCESSED_KEY, new Date().toISOString()).catch(
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
// Record queue wait time and completion on the OTel span
|
||||
|
||||
@@ -2,10 +2,13 @@ import {
|
||||
ANALYTICS_BAKED,
|
||||
ANALYTICS_EVENTS,
|
||||
APP_VERSION,
|
||||
type FeedbackDiscoverySource,
|
||||
type FeedbackErrorCategory,
|
||||
type FeedbackFrictionArea,
|
||||
type FeedbackImportantArea,
|
||||
type FeedbackInstallMethod,
|
||||
type FeedbackPriorTool,
|
||||
type FeedbackSelfHostMotivation,
|
||||
type FeedbackSentiment,
|
||||
type FeedbackSource,
|
||||
type FeedbackSurveyId,
|
||||
@@ -38,6 +41,11 @@ export interface FeedbackEventProperties {
|
||||
usage_type?: FeedbackUsageType;
|
||||
important_areas?: FeedbackImportantArea[];
|
||||
friction_area?: FeedbackFrictionArea;
|
||||
// Onboarding survey (telemetry-blind) answers: what they used before, why they
|
||||
// self-host, and how they found SnapOtter. See analytics/feedback.ts.
|
||||
prior_tool?: FeedbackPriorTool;
|
||||
selfhost_motivation?: FeedbackSelfHostMotivation;
|
||||
discovery_source?: FeedbackDiscoverySource;
|
||||
error_category?: FeedbackErrorCategory;
|
||||
}
|
||||
|
||||
@@ -138,6 +146,9 @@ function cleanFeedbackProperties(properties: FeedbackEventProperties): Record<st
|
||||
copyString("install_method");
|
||||
copyString("usage_type");
|
||||
copyString("friction_area");
|
||||
copyString("prior_tool");
|
||||
copyString("selfhost_motivation");
|
||||
copyString("discovery_source");
|
||||
copyString("error_category");
|
||||
|
||||
if (properties.important_areas?.length) {
|
||||
|
||||
@@ -15,6 +15,16 @@ export async function upsertSetting(key: string, value: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a setting only if the key does not already exist (first-write-wins).
|
||||
* Uses ON CONFLICT DO NOTHING, so repeated calls are cheap no-ops after the
|
||||
* first and the original value is preserved. Used for one-time markers like the
|
||||
* instance's first successful processing that gate the onboarding survey.
|
||||
*/
|
||||
export async function setSettingIfAbsent(key: string, value: string): Promise<void> {
|
||||
await db.insert(schema.settings).values({ key, value }).onConflictDoNothing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a numeric setting from the DB `settings` table.
|
||||
* Returns `defaultValue` when the key is missing, non-numeric, or on DB error.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
FEEDBACK_DISCOVERY_SOURCE_VALUES,
|
||||
FEEDBACK_ERROR_CATEGORY_VALUES,
|
||||
FEEDBACK_FRICTION_AREA_VALUES,
|
||||
FEEDBACK_IMPORTANT_AREA_VALUES,
|
||||
FEEDBACK_INSTALL_METHOD_VALUES,
|
||||
FEEDBACK_PRIOR_TOOL_VALUES,
|
||||
FEEDBACK_SELFHOST_MOTIVATION_VALUES,
|
||||
FEEDBACK_SENTIMENT_VALUES,
|
||||
FEEDBACK_SOURCE_VALUES,
|
||||
FEEDBACK_SURVEY_ID_VALUES,
|
||||
@@ -58,6 +61,9 @@ const feedbackBodySchema = z
|
||||
usageType: z.enum(FEEDBACK_USAGE_TYPE_VALUES).optional(),
|
||||
importantAreas: z.array(z.enum(FEEDBACK_IMPORTANT_AREA_VALUES)).max(5).optional(),
|
||||
frictionArea: z.enum(FEEDBACK_FRICTION_AREA_VALUES).optional(),
|
||||
priorTool: z.enum(FEEDBACK_PRIOR_TOOL_VALUES).optional(),
|
||||
selfHostMotivation: z.enum(FEEDBACK_SELFHOST_MOTIVATION_VALUES).optional(),
|
||||
discoverySource: z.enum(FEEDBACK_DISCOVERY_SOURCE_VALUES).optional(),
|
||||
errorCategory: z.enum(FEEDBACK_ERROR_CATEGORY_VALUES).optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
@@ -97,6 +103,9 @@ function toPostHogProperties(body: z.infer<typeof feedbackBodySchema>): Feedback
|
||||
usage_type: body.usageType,
|
||||
important_areas: body.importantAreas,
|
||||
friction_area: body.frictionArea,
|
||||
prior_tool: body.priorTool,
|
||||
selfhost_motivation: body.selfHostMotivation,
|
||||
discovery_source: body.discoverySource,
|
||||
error_category: body.errorCategory,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user