Skip to content

feat: add robust source form validation and error reporting #923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

knudtty
Copy link
Contributor

@knudtty knudtty commented Jun 10, 2025

This PR deprecates the existing zod source schema, which was not adequately handling the different cases for required vs. optional parameters across different source types. The new zod schema is a discriminated union, built from the different SourceKinds. With its inferred type, TSourceUnion, you can now switch on kind and Typescript will resolve which fields are valid for that kind of source. We should adopt this type across the app wherever we can instead of TSource.

TSource was previously inferred from the original source schema and is heavily used across the application and not yet ready for removal. For compatibility, TSource is now constructed with some type magic from the new TSourceUnion.

Fixes HDX-1768
Fixes HDX-1804

Copy link

changeset-bot bot commented Jun 10, 2025

🦋 Changeset detected

Latest commit: b749dd6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/common-utils Patch
@hyperdx/app Patch
@hyperdx/api Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

vercel bot commented Jun 10, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
hyperdx-v2-oss-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 11, 2025 3:15pm

Comment on lines +43 to +47
// TODO: HDX-1768 Eliminate type assertion
const source = await createSource(teamId.toString(), {
...req.body,
team: teamId,
});
} as any);
Copy link
Contributor Author

@knudtty knudtty Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because TSource and the new TSourceUnion are both used in some areas, some type assertions are needed to make things compile right now. I tagged all type assertions with a TODO comment and the linear issue where they were added.

I plan to follow up with a Part 2 PR to switch all instances of TSource to TSourceUnion on the frontend, or just eliminate TSource entirely. This should eliminate nearly all type assertions

Comment on lines +916 to +931
notifications.show({
color: 'red',
message: (
<Stack>
<Text size="sm">
<b>Failed to create source</b>
</Text>
{errors.map((err, i) => (
<Text key={i} size="sm">
✖ {err.message}
</Text>
))}
</Stack>
),
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display a toast with the error information. You can test this out by trying to add a session without a Correlated Trace Source selected, or by any source without a timestamp column defined.

if (parseResult.error) {
handleError(parseResult.error);
return;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the error handling for creating and saving sources. I tried using zodResolver but couldn't get it working.

Comment on lines +1077 to +1098
<FormRow label={'Server Connection'}>
<ConnectionSelectControlled control={control} name={`connection`} />
</FormRow>
<FormRow label={'Database'}>
<DatabaseSelectControlled
control={control}
name={`from.databaseName`}
connectionId={connectionId}
/>
</FormRow>
{kind !== SourceKind.Metric && (
<FormRow label={'Table'}>
<DBTableSelectControlled
database={databaseName}
control={control}
name={`from.tableName`}
connectionId={connectionId}
rules={{ required: 'Table is required' }}
/>
</FormRow>
)}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved essential common components to a shared location

Comment on lines +27 to +40
export const MetricTableSchema = z
.object(
Object.values(MetricsDataType).reduce(
(acc, key) => ({
...acc,
[key]: z.string().optional(),
}),
{} as Record<MetricsDataType, z.ZodString>,
),
)
.refine(
tables => Object.values(tables).some(table => table && table.length > 0),
{ message: 'At least one metric table must be specified' },
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref: HDX-1804

Comment on lines 546 to 556
const SourceFormBaseSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Name is required'),
kind: z.nativeEnum(SourceKind),
connection: z.string().min(1, 'Server Connection is required'),
from: z.object({
databaseName: z.string().min(1, 'Database is required'),
tableName: z.string().min(1, 'Table is required'),
}),
timestampValueExpression: z.string().min(1, 'Timestamp Column is required'),
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common across all source types

Comment on lines 648 to 654
export const SourceFormSchema = z.discriminatedUnion('kind', [
SourceFormBaseSchema.extend(LogSourceFormAugmentation),
SourceFormBaseSchema.extend(TraceSourceFormAugmentation),
SourceFormBaseSchema.extend(SessionSourceFormAugmentation),
SourceFormBaseSchema.extend(MetricSourceFormAugmentation),
]);
export type TSourceForm = z.infer<typeof SourceFormSchema>;
Copy link
Contributor Author

@knudtty knudtty Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the magic happens. SourceFormSchema and TSourceUnion is a tagged union. You can switch on the 'kind' field of the different source types and typescript will know which fields are valid for that type. I want to replace TSource everywhere possible with TSourceUnion. It will eliminate an entire class of bugs where we wrongly assume a source is a specific kind, but that field doesn't exist on that source because it's a different kind

Comment on lines -484 to -487
export const SourceSchema = z.object({
// --------------------------
// TABLE SOURCE FORM VALIDATION
// --------------------------

// Base schema with fields common to all source types
const SourceBaseSchema = z.object({
id: z.string(),
name: z.string().min(1, 'Name is required'),
kind: z.nativeEnum(SourceKind),
connection: z.string().min(1, 'Server Connection is required'),
from: z.object({
databaseName: z.string(),
tableName: z.string(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove previous zod SourceSchema

? never
: K]?: T extends infer U ? (K extends keyof U ? U[K] : never) : never;
};
export type TSource = FlattenUnion<z.infer<typeof SourceSchema>>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some type magic happening, but this preserves TSource compatibility while being based off of the new union zod schema

@knudtty knudtty force-pushed the aaron/source-form-validation branch 3 times, most recently from 980d2ac to 17146f4 Compare June 11, 2025 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant