Skip to main content

Command Palette

Search for a command to run...

How I contribute to Sentry's Alert Feature

Updated
3 min read

Identifying the Issue.

Issue #115268 https://github.com/getsentry/sentry/issues/115268

The issue is about a bug on Alert feature, where a user can't create an Alert when it selects 'Tagged Event', even though they have filled all of the required fields. The validation 'Ensure all fields are filled in' will always appear as a blocker.

Diving into the source code

I tried to search which function causes this.

Step 1 - Read the error message location.

export function validateTaggedEventCondition({
  condition,
}: ValidateDataConditionProps): string | undefined {
  if (!condition.comparison.key || !condition.comparison.match) {
    return t('Ensure all fields are filled in.');
  }
  if (matchRequiresValue(condition.comparison.match) && !condition.comparison.value) {
    return t('Ensure all fields are filled in.');
  }
  return undefined;
}

The validation 'Ensure all fields are filled in' always appear, so either !condition.comparison.key or !condition.comparison.match is undefined or null. It seems the key was never actually saved into state, even though the user typed something.

When picking Tagged Event, we will be asked to input a key, an operator, and a value. Even though the key has been selected automatically, which is Tag, whatever value we typed, is not identified. So the validation always appear.

Step 2 - Trace where key gets set

return (
    <AutomationBuilderSelect
      disabled={isLoading}
      creatable
      name={`${condition_id}.comparison.key`}
      aria-label={t('Tag')}
      placeholder={isLoading ? t('Loading tags\u2026') : t('tag')}
      value={condition.comparison.key}
      options={sortedOptions}
      onChange={(e: SelectValue<MatchType>) => {
        onUpdate({comparison: {...condition.comparison, key: e.value}});
        removeError(condition.id);
      }}
    />
  );
}

This onChange is set on KeyField's component. When does onChange is fired?

When we look more into this code, we can see that it uses creatable property. It means the user can type in a customized value that isn't in the dropdown list. In react-select, the Creatable component only fires onChange in two cases:

  1. User presses Enter to confirm the typed value

  2. User clicks a suggestion from the dropdown

So even though React has already filled automatically Tag in the key field, once the user clicks the next field (either Match or Value), which will become blur -> onChange is never fired -> condition.comparison.key is still undefined.

Step 3 - Add a solution

My solution to this problem would be to hook onBlur and manually commit whatever was typed - which will required useRef & onInputChange hook.

I use onInputChange to track every keystroke while typing.

I use useRef to interact with the value that is ref is listening to.

I use onBlur to set what happens when a user clicks away or when the field loses focus.

I use onChange to track when a value is explicitly selected/committed.

return (
    <AutomationBuilderSelect
      disabled={isLoading}
      creatable
      name={`${condition_id}.comparison.key`}
      aria-label={t('Tag')}
      placeholder={isLoading ? t('Loading tags\u2026') : t('tag')}
      value={condition.comparison.key}
      options={sortedOptions}
      onInputChange={(value: string, {action}: {action: string}) => {
        // 'menu-close' and 'input-blur' fire with '' right before onBlur and would
        // wipe the tracked value, so only track real typing actions
        if (action === 'input-change') {
          typedValueRef.current = value;
        }
      }}
      onBlur={() => {
        if (typedValueRef.current) {
          onUpdate({comparison: {...condition.comparison, key: typedValueRef.current}});
          removeError(condition.id);
        }
      }}
      onChange={(e: SelectValue<MatchType>) => {
        typedValueRef.current = '';
        onUpdate({comparison: {...condition.comparison, key: e.value}});
        removeError(condition.id);
        typedValueRef.current = '';
      }}
    />
  );
}

I then also change the validation logic:

export function validateTaggedEventCondition({
  condition,
}: ValidateDataConditionProps): string | undefined {
  if (!condition.comparison.key) {
    return t('Select a tag.');
  }
  if (!condition.comparison.match) {
    return t('Select a match type.');
  }
  if (matchRequiresValue(condition.comparison.match) && !condition.comparison.value) {
    return t('Enter a value.');
  }
  return undefined;
}

Create The Pull Request

Finally, after pushing the commit to remote branch, I create the PR : https://github.com/getsentry/sentry/pull/116120