Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 6 additions & 6 deletions apps/backend/src/routes/public-file-serving.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Public asset serving — `GET /files/*`.
*
* Stored public files (avatars today, written by the org/project avatar RPCs to
* `avatars/<entity>/<id>/<sha256>.<ext>`) are content-addressed, immutable, and
* unauthenticated: the key's sha256 is the capability. These are images served
* cross-origin as `<img src>`, so — unlike the paywall HTML routesthere is
* NO CSP sandbox; just the stored `Content-Type`, an immutable cache policy,
* permissive CORS, and `nosniff`.
* Stored public files are unauthenticated images served cross-origin as
* `<img src>`. Most use immutable content-addressed keys; mutable paywall
* thumbnails append their document sequence to the public URL as a cache
* buster. Unlike the paywall HTML routes, there is NO CSP sandbox; just the
* stored `Content-Type`, an immutable cache policy, permissive CORS, and
* `nosniff`.
*/
import { PublicFileStore } from "@voidhash/core/services";
import { Cause, Effect, Layer } from "effect";
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/routes/v1/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export const SdkGroupLive = HttpApiBuilder.group(VoidhashV1Api, "sdk", (handlers
return null;
}

// `resolved.exposure` (when non-null) carries { experimentKey,
// `resolved.exposure` (when non-null) carries { experimentId,
// variantKey, personId, distinctId } for the assigned subject.
// Server-side `$experiment.exposed` emission is wired here once the
// analytics dispatch producer (`AnalyticsDispatchService` over the
Expand Down
78 changes: 10 additions & 68 deletions apps/backend/src/rpcs/experiment-rpcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import { ClickhouseWebClient } from "@voidhash/clickhouse-db/clickhouse-client-w
import {
ExperimentRpcsDef,
RpcActionForbiddenError,
RpcExperimentKeyAlreadyExistsError,
RpcExperimentNotFoundError,
RpcExperimentServiceError,
RpcExperimentTreatmentNotFoundError,
RpcExperimentValidationError,
RpcExperimentVariantNotFoundError,
} from "@voidhash/rpc";
Expand All @@ -26,9 +24,8 @@ const toRpcExperiment = (e: {
readonly featureFlagId: string;
readonly hypothesis: string | null;
readonly id: string;
readonly key: string;
readonly name: string;
readonly primaryMetricEventName: string;
readonly primaryMetricEventName: string | null;
readonly projectId: string;
readonly secondaryMetricEventNames: readonly string[] | null;
readonly startedAt: Date | null;
Expand All @@ -43,7 +40,6 @@ const toRpcExperiment = (e: {
readonly experimentId: string;
readonly id: string;
readonly isControl: boolean;
readonly key: string;
readonly name: string;
readonly updatedAt: Date | null;
readonly weightBps: number;
Expand Down Expand Up @@ -81,7 +77,6 @@ const toRpcExperiment = (e: {
featureFlagId: e.featureFlagId,
hypothesis: e.hypothesis,
id: e.id,
key: e.key,
name: e.name,
primaryMetricEventName: e.primaryMetricEventName,
projectId: e.projectId,
Expand Down Expand Up @@ -116,12 +111,6 @@ export const ExperimentRpcsLive = ExperimentRpcsDef.toLayer(
Effect.fail(
new RpcExperimentVariantNotFoundError({ message: `Variant not found: ${error.variantId}` }),
);
const treatmentNotFound = (error: { readonly treatmentId: string }) =>
Effect.fail(
new RpcExperimentTreatmentNotFoundError({
message: `Treatment not found: ${error.treatmentId}`,
}),
);

return {
ArchiveExperiment: (input) =>
Expand All @@ -142,21 +131,12 @@ export const ExperimentRpcsLive = ExperimentRpcsDef.toLayer(
}),
),
CreateExperiment: (input) =>
service
.createExperiment({
...input,
secondaryMetricEventNames: input.secondaryMetricEventNames
? [...input.secondaryMetricEventNames]
: undefined,
})
.pipe(
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentKeyAlreadyExistsError: (error) =>
Effect.fail(new RpcExperimentKeyAlreadyExistsError({ key: error.key })),
ExperimentServiceError: serviceError,
}),
),
service.createExperiment(input).pipe(
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentServiceError: serviceError,
}),
),
GetExperiment: (input) =>
service.getExperiment(input).pipe(
Effect.map(toRpcExperiment),
Expand Down Expand Up @@ -190,25 +170,6 @@ export const ExperimentRpcsLive = ExperimentRpcsDef.toLayer(
ExperimentValidationError: validation,
}),
),
RemoveExperimentTreatment: (input) =>
service.removeTreatment(input).pipe(
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentNotFoundError: notFound,
ExperimentServiceError: serviceError,
ExperimentTreatmentNotFoundError: treatmentNotFound,
ExperimentValidationError: validation,
}),
),
ReplaceExperimentVariants: (input) =>
service.replaceVariants({ ...input, variants: [...input.variants] }).pipe(
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentNotFoundError: notFound,
ExperimentServiceError: serviceError,
ExperimentValidationError: validation,
}),
),
RestoreExperiment: (input) =>
service.restoreExperiment(input).pipe(
Effect.catchTags({
Expand All @@ -226,28 +187,9 @@ export const ExperimentRpcsLive = ExperimentRpcsDef.toLayer(
ExperimentValidationError: validation,
}),
),
UpdateExperiment: (input) =>
service
.updateExperiment({
...input,
secondaryMetricEventNames:
input.secondaryMetricEventNames === undefined
? undefined
: input.secondaryMetricEventNames
? [...input.secondaryMetricEventNames]
: null,
})
.pipe(
Effect.map(toRpcExperiment),
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentNotFoundError: notFound,
ExperimentServiceError: serviceError,
ExperimentValidationError: validation,
}),
),
UpsertExperimentTreatment: (input) =>
service.upsertTreatment(input).pipe(
SaveExperimentSetup: (input) =>
service.saveSetup(input).pipe(
Effect.map(toRpcExperiment),
Effect.catchTags({
ActionForbiddenError: forbidden,
ExperimentNotFoundError: notFound,
Expand Down
54 changes: 49 additions & 5 deletions apps/backend/src/rpcs/feature-flag-rpcs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { FeatureFlagService } from "@voidhash/core/services";
import type {
FeatureFlag,
FeatureFlagOverride,
FeatureFlagTarget,
FeatureFlagVariant,
} from "@voidhash/db";
import {
FeatureFlagRpcsDef,
RpcActionForbiddenError,
Expand All @@ -11,6 +17,41 @@ import {
} from "@voidhash/rpc";
import { Effect } from "effect";

const toRpcFeatureFlagVariant = (variant: FeatureFlagVariant) => ({
archivedAt: variant.archivedAt,
createdAt: variant.createdAt,
featureFlagId: variant.featureFlagId,
id: variant.id,
label: variant.name || null,
updatedAt: variant.updatedAt,
value: variant.payload,
});

const toRpcFeatureFlag = (
flag: FeatureFlag & {
readonly overrides: ReadonlyArray<FeatureFlagOverride>;
readonly targets: ReadonlyArray<FeatureFlagTarget>;
readonly variants: ReadonlyArray<FeatureFlagVariant>;
},
) => {
const { key, name: _name, variants, ...rest } = flag;
return {
...rest,
slug: key,
variants: variants.map(toRpcFeatureFlagVariant),
};
};

const toRpcFeatureFlagListItem = (
flag: FeatureFlag & { readonly variantCount: number; readonly variants?: undefined },
) => {
const { key, name: _name, variants: _variants, ...rest } = flag;
return {
...rest,
slug: key,
};
};

export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
Effect.gen(function* FeatureFlagRpcsLive() {
const service = yield* FeatureFlagService;
Expand Down Expand Up @@ -54,8 +95,8 @@ export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
Effect.fail(new RpcFeatureFlagTargetNotFoundError({ message: error.message })),
}),
),
CreateFeatureFlag: (input) =>
service.createFlag(input).pipe(
CreateFeatureFlag: ({ slug, ...input }) =>
service.createFlag({ ...input, key: slug }).pipe(
Effect.catchTags({
ActionForbiddenError: (error) =>
Effect.fail(new RpcActionForbiddenError({ message: error.message })),
Expand All @@ -69,6 +110,7 @@ export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
),
GetFeatureFlag: ({ id }) =>
service.getFlagById({ id }).pipe(
Effect.map(toRpcFeatureFlag),
Effect.catchTags({
ActionForbiddenError: (error) =>
Effect.fail(new RpcActionForbiddenError({ message: error.message })),
Expand Down Expand Up @@ -100,6 +142,7 @@ export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
),
ListFeatureFlags: (input) =>
service.listFlags(input).pipe(
Effect.map((flags) => flags.map(toRpcFeatureFlagListItem)),
Effect.catchTags({
ActionForbiddenError: (error) =>
Effect.fail(new RpcActionForbiddenError({ message: error.message })),
Expand All @@ -120,8 +163,9 @@ export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
Effect.fail(new RpcFeatureFlagServiceError({ cause: error.cause })),
}),
),
UpdateFeatureFlag: (input) =>
service.updateFlag(input).pipe(
UpdateFeatureFlag: ({ slug, ...input }) =>
service.updateFlag({ ...input, key: slug }).pipe(
Effect.map(toRpcFeatureFlag),
Effect.catchTags({
ActionForbiddenError: (error) =>
Effect.fail(new RpcActionForbiddenError({ message: error.message })),
Expand All @@ -137,7 +181,7 @@ export const FeatureFlagRpcsLive = FeatureFlagRpcsDef.toLayer(
),
UpdateFeatureFlagVariants: (input) =>
service
.updateFlagVariants({
.updateCustomerFlagVariants({
...input,
variants: [...input.variants],
})
Expand Down
38 changes: 1 addition & 37 deletions apps/backend/src/rpcs/paywall-rpcs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
MimicHost,
PaywallReleaseService,
PaywallService,
PaywallThumbnailService,
} from "@voidhash/core/services";
import { MimicHost, PaywallReleaseService, PaywallService } from "@voidhash/core/services";
import {
PaywallRpcsDef,
RpcActionForbiddenError,
Expand All @@ -19,40 +14,9 @@ export const PaywallRpcsLive = PaywallRpcsDef.toLayer(
Effect.gen(function* () {
const paywallService = yield* PaywallService;
const releaseService = yield* PaywallReleaseService;
const thumbnailService = yield* PaywallThumbnailService;
const mimicHost = yield* MimicHost;

return {
BackfillPaywallThumbnails: ({ projectId }) =>
Effect.gen(function* () {
const projectPaywalls = yield* paywallService.getPaywalls(projectId, true);
const missing = projectPaywalls.filter((paywall) => paywall.thumbnailUrl === null);
const results = yield* Effect.forEach(
missing,
(paywall) =>
thumbnailService.renderCurrent(paywall.id).pipe(
Effect.as(true),
Effect.catchTag("PaywallThumbnailServiceError", (error) =>
Effect.logError("Paywall thumbnail backfill failed", {
paywallId: paywall.id,
message: error.message,
}).pipe(Effect.as(false)),
),
),
{ concurrency: 1 },
);
return {
attempted: missing.length,
rendered: results.filter(Boolean).length,
};
}).pipe(
Effect.catchTags({
ActionForbiddenError: (error) =>
Effect.fail(new RpcActionForbiddenError({ message: error.message })),
PaywallServiceError: (error) =>
Effect.fail(new RpcPaywallServiceError({ cause: error.cause })),
}),
),
CreatePaywall: (input) =>
paywallService.createPaywall(input).pipe(
Effect.catchTags({
Expand Down
23 changes: 6 additions & 17 deletions apps/backend/src/testing/rpc-smoke-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,10 @@ export const rpcSmokeCases = [
expected: success,
payload: ({ ids, runId }) => ({
description: "Smoke flag",
key: `smoke-flag-${runId}`,
name: "Smoke Flag",
projectId: ids.projectId,
slug: `smoke-flag-${runId}`,
type: "json",
variants: [],
}),
role: "admin",
tag: "CreateFeatureFlag",
Expand All @@ -277,8 +278,8 @@ export const rpcSmokeCases = [
payload: ({ featureFlagId, runId }) => ({
enabled: true,
id: featureFlagId,
name: `Smoke Flag ${runId}`,
rolloutBps: 5000,
slug: `smoke-flag-updated-${runId}`,
}),
role: "admin",
tag: "UpdateFeatureFlag",
Expand All @@ -289,10 +290,7 @@ export const rpcSmokeCases = [
featureFlagId,
variants: [
{
key: "control",
name: "Control",
payload: { kind: "control" },
weightBps: 10000,
value: { kind: "control" },
},
],
}),
Expand Down Expand Up @@ -555,12 +553,6 @@ export const rpcSmokeCases = [
role: "admin",
tag: "CreatePaywall",
},
{
expected: success,
payload: ({ ids }) => ({ projectId: ids.projectId }),
role: "admin",
tag: "BackfillPaywallThumbnails",
},
{
expected: success,
payload: ({ ids }) => ({ includeArchived: true, projectId: ids.projectId }),
Expand Down Expand Up @@ -792,10 +784,7 @@ const knownMissingRpcSmokeTags = new Set([
"ListExperiments",
"GetExperiment",
"CreateExperiment",
"UpdateExperiment",
"ReplaceExperimentVariants",
"UpsertExperimentTreatment",
"RemoveExperimentTreatment",
"SaveExperimentSetup",
"StartExperiment",
"PauseExperiment",
"ConcludeExperiment",
Expand Down
2 changes: 0 additions & 2 deletions apps/mimic-admin/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const buttonVariants = cva(
"bg-destructive text-white shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
Expand Down
Loading
Loading