mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-10-30 18:34:19 +00:00
The merge 26a9531 ("merge: upstream/main (sync, brand preserved)") took a
branded 817-line example and an 1196-line upstream rewrite of the same file
and interleaved them. Test bodies ended up under the wrong test names, an
afterEach landed inside a test body, and orphaned statements sat at top level
after a closing paren. Neither example.test.ts nor example.ts parsed. Only
the test file failed the unit gate, because example.ts is not a test suite --
it is the module the suite drives, so its damage showed up as a suite that
could not run rather than as an error of its own.
Both files are restored from the upstream side, which is the one that moved:
our side of the merge contributed only the brand rename, while upstream had
reworked feature flags into local evaluation (queries) with remote evaluation
split off into separate actions. That is what the shipped client does today,
so the upstream side is the one that describes it. The rename is reapplied on
top -- POSTHOG_ to INSIGHTS_, posthog-node to insights-node, components.posthog
to components.insights, PostHog to Insights.
Restoring the pair also gives example.ts back the testThrowError,
testEvaluateFlag, testEvaluateFlagPayload and testEvaluateAllFlags exports
that the scramble had dropped while App.tsx was still calling them.
Two things did not survive as-is. Events now post to ${host}/v1/e, not the
/batch/ route the tests were written against, so the helper that picks event
calls out of the fetch log matched nothing and every capture, identify and
captureException assertion read an empty event; the path is now a named
constant. And upstream had no groupIdentify or alias wrappers, so taking its
example.ts wholesale would have left the component's handlers for both with
no end-to-end coverage -- they are restored from our pre-merge side, moved to
the deterministic scheduler helper and the INSIGHTS_PROJECT_TOKEN the
component actually reads, plus a case for the group-derived distinct_id
fallback that nothing else exercised.
Verified: packages/convex pnpm test:unit completes in ~5s, 8 suites and 151
tests passing, where before the suite could not be parsed. Component coverage
rises to 84.65% of statements and 100% of functions. Repo-wide pnpm build is
34/34. No occurrence of the upstream brand remains in either file.
345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import { insights } from './insights.js'
|
|
import { action, mutation, query } from './_generated/server.js'
|
|
import { components } from './_generated/api.js'
|
|
import { v } from 'convex/values'
|
|
|
|
// --- Fire-and-forget methods (mutations) ---
|
|
// When the identify callback is configured, distinctId is resolved automatically
|
|
// from the signed-in user. Pass distinctId explicitly to override or when the
|
|
// user is not signed in.
|
|
|
|
export const testCapture = mutation({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
event: v.string(),
|
|
properties: v.optional(v.any()),
|
|
groups: v.optional(v.any()),
|
|
sendFeatureFlags: v.optional(v.boolean()),
|
|
timestamp: v.optional(v.string()),
|
|
uuid: v.optional(v.string()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await insights.capture(ctx, {
|
|
distinctId: args.distinctId,
|
|
event: args.event,
|
|
properties: args.properties,
|
|
groups: args.groups,
|
|
sendFeatureFlags: args.sendFeatureFlags,
|
|
timestamp: args.timestamp ? new Date(args.timestamp) : undefined,
|
|
uuid: args.uuid || undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
})
|
|
return { success: true }
|
|
},
|
|
})
|
|
|
|
export const testIdentify = mutation({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
properties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await insights.identify(ctx, {
|
|
distinctId: args.distinctId,
|
|
properties: args.properties,
|
|
disableGeoip: args.disableGeoip,
|
|
})
|
|
return { success: true }
|
|
},
|
|
})
|
|
|
|
export const testGroupIdentify = mutation({
|
|
args: {
|
|
groupType: v.string(),
|
|
groupKey: v.string(),
|
|
properties: v.optional(v.any()),
|
|
distinctId: v.optional(v.string()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await insights.groupIdentify(ctx, {
|
|
groupType: args.groupType,
|
|
groupKey: args.groupKey,
|
|
properties: args.properties,
|
|
distinctId: args.distinctId || undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
})
|
|
return { success: true }
|
|
},
|
|
})
|
|
|
|
export const testAlias = mutation({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
alias: v.string(),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await insights.alias(ctx, {
|
|
distinctId: args.distinctId,
|
|
alias: args.alias,
|
|
disableGeoip: args.disableGeoip,
|
|
})
|
|
return { success: true }
|
|
},
|
|
})
|
|
|
|
export const testCaptureException = mutation({
|
|
args: {
|
|
errorMessage: v.string(),
|
|
errorType: v.optional(v.union(v.literal('error'), v.literal('string'), v.literal('object'))),
|
|
distinctId: v.optional(v.string()),
|
|
additionalProperties: v.optional(v.any()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
let error: unknown
|
|
switch (args.errorType ?? 'error') {
|
|
case 'error':
|
|
error = new Error(args.errorMessage)
|
|
break
|
|
case 'string':
|
|
error = args.errorMessage
|
|
break
|
|
case 'object':
|
|
error = { message: args.errorMessage }
|
|
break
|
|
}
|
|
|
|
await insights.captureException(ctx, {
|
|
error,
|
|
distinctId: args.distinctId || undefined,
|
|
additionalProperties: args.additionalProperties,
|
|
})
|
|
return { success: true }
|
|
},
|
|
})
|
|
|
|
export const testThrowError = mutation({
|
|
args: {
|
|
errorMessage: v.string(),
|
|
},
|
|
handler: async (_ctx, args) => {
|
|
throw new Error(args.errorMessage)
|
|
},
|
|
})
|
|
|
|
// --- Feature flag methods (actions) ---
|
|
|
|
const featureFlagArgs = {
|
|
distinctId: v.optional(v.string()),
|
|
flagKey: v.string(),
|
|
groups: v.optional(v.any()),
|
|
personProperties: v.optional(v.any()),
|
|
groupProperties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
}
|
|
|
|
function featureFlagOptions(args: {
|
|
groups?: unknown
|
|
personProperties?: unknown
|
|
groupProperties?: unknown
|
|
disableGeoip?: boolean
|
|
}) {
|
|
return {
|
|
groups: args.groups as Record<string, string> | undefined,
|
|
personProperties: args.personProperties as Record<string, string> | undefined,
|
|
groupProperties: args.groupProperties as Record<string, Record<string, string>> | undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
}
|
|
}
|
|
|
|
export const testGetFeatureFlag = query({
|
|
args: featureFlagArgs,
|
|
handler: async (ctx, args) => {
|
|
const value = await insights.getFeatureFlag(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
...featureFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, value: value ?? null }
|
|
},
|
|
})
|
|
|
|
export const testIsFeatureEnabled = query({
|
|
args: featureFlagArgs,
|
|
handler: async (ctx, args) => {
|
|
const enabled = await insights.isFeatureEnabled(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
...featureFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, enabled: enabled ?? null }
|
|
},
|
|
})
|
|
|
|
export const testGetFeatureFlagPayload = query({
|
|
args: {
|
|
...featureFlagArgs,
|
|
matchValue: v.optional(v.union(v.boolean(), v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const payload = await insights.getFeatureFlagPayload(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
matchValue: args.matchValue,
|
|
...featureFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, payload }
|
|
},
|
|
})
|
|
|
|
export const testGetFeatureFlagResult = query({
|
|
args: featureFlagArgs,
|
|
handler: async (ctx, args) => {
|
|
const result = await insights.getFeatureFlagResult(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
...featureFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, result: result ?? null }
|
|
},
|
|
})
|
|
|
|
export const testGetAllFlags = query({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
groups: v.optional(v.any()),
|
|
personProperties: v.optional(v.any()),
|
|
groupProperties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
flagKeys: v.optional(v.array(v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const flags = await insights.getAllFlags(ctx, {
|
|
distinctId: args.distinctId,
|
|
groups: args.groups as Record<string, string> | undefined,
|
|
personProperties: args.personProperties as Record<string, string> | undefined,
|
|
groupProperties: args.groupProperties as Record<string, Record<string, string>> | undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
flagKeys: args.flagKeys,
|
|
})
|
|
return { flags }
|
|
},
|
|
})
|
|
|
|
// --- Cache inspection helpers (used by the demo UI to surface the cron's progress) ---
|
|
|
|
export const flagDefinitionsStatus = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const row = await ctx.runQuery(components.insights.lib.getFlagDefinitions, {})
|
|
if (!row) return null
|
|
let flagKeys: string[] = []
|
|
try {
|
|
const parsed = JSON.parse(row.data) as { flags?: Array<{ key?: string }> }
|
|
flagKeys = parsed.flags?.map((f) => f.key ?? '<unnamed>') ?? []
|
|
} catch {
|
|
// ignore parse errors — keys list stays empty.
|
|
}
|
|
return {
|
|
fetchedAt: row.fetchedAt,
|
|
etag: row.etag ?? null,
|
|
flagCount: flagKeys.length,
|
|
flagKeys,
|
|
}
|
|
},
|
|
})
|
|
|
|
// --- Remote feature flag evaluation wrappers ---
|
|
//
|
|
// These are action-context actions that hit Insights's `/flags` endpoint via the client's
|
|
// `evaluate*` methods. Use them when local eval isn't possible (no personal API key, experience
|
|
// continuity flags, static cohorts, properties you don't have server-side).
|
|
|
|
const remoteFlagArgs = {
|
|
distinctId: v.optional(v.string()),
|
|
flagKey: v.string(),
|
|
groups: v.optional(v.any()),
|
|
personProperties: v.optional(v.any()),
|
|
groupProperties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
}
|
|
|
|
function remoteFlagOptions(args: {
|
|
groups?: unknown
|
|
personProperties?: unknown
|
|
groupProperties?: unknown
|
|
disableGeoip?: boolean
|
|
}) {
|
|
return {
|
|
groups: args.groups as Record<string, string> | undefined,
|
|
personProperties: args.personProperties as Record<string, unknown> | undefined,
|
|
groupProperties: args.groupProperties as Record<string, Record<string, unknown>> | undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
}
|
|
}
|
|
|
|
export const testEvaluateFlag = action({
|
|
args: remoteFlagArgs,
|
|
handler: async (ctx, args) => {
|
|
const value = await insights.evaluateFlag(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
...remoteFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, value }
|
|
},
|
|
})
|
|
|
|
export const testEvaluateFlagPayload = action({
|
|
args: remoteFlagArgs,
|
|
handler: async (ctx, args) => {
|
|
const payload = await insights.evaluateFlagPayload(ctx, {
|
|
key: args.flagKey,
|
|
distinctId: args.distinctId,
|
|
...remoteFlagOptions(args),
|
|
})
|
|
return { flagKey: args.flagKey, payload }
|
|
},
|
|
})
|
|
|
|
export const testEvaluateAllFlags = action({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
groups: v.optional(v.any()),
|
|
personProperties: v.optional(v.any()),
|
|
groupProperties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
flagKeys: v.optional(v.array(v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
return await insights.evaluateAllFlags(ctx, {
|
|
distinctId: args.distinctId,
|
|
groups: args.groups as Record<string, string> | undefined,
|
|
personProperties: args.personProperties as Record<string, unknown> | undefined,
|
|
groupProperties: args.groupProperties as Record<string, Record<string, unknown>> | undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
flagKeys: args.flagKeys,
|
|
})
|
|
},
|
|
})
|
|
|
|
export const testGetAllFlagsAndPayloads = query({
|
|
args: {
|
|
distinctId: v.optional(v.string()),
|
|
groups: v.optional(v.any()),
|
|
personProperties: v.optional(v.any()),
|
|
groupProperties: v.optional(v.any()),
|
|
disableGeoip: v.optional(v.boolean()),
|
|
flagKeys: v.optional(v.array(v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const result = await insights.getAllFlagsAndPayloads(ctx, {
|
|
distinctId: args.distinctId,
|
|
groups: args.groups as Record<string, string> | undefined,
|
|
personProperties: args.personProperties as Record<string, string> | undefined,
|
|
groupProperties: args.groupProperties as Record<string, Record<string, string>> | undefined,
|
|
disableGeoip: args.disableGeoip,
|
|
flagKeys: args.flagKeys,
|
|
})
|
|
return result
|
|
},
|
|
})
|