Commit 300f4f48 authored by Bob Van Landuyt's avatar Bob Van Landuyt 💬
Browse files

Merge branch 'feat/custom-category-schema' into 'master'

feat: Preload feature categories JSON schema

See merge request !229

Merged-by: Bob Van Landuyt's avatarBob Van Landuyt <bob@gitlab.com>
Approved-by: Bob Van Landuyt's avatarBob Van Landuyt <bob@gitlab.com>
Reviewed-by: default avatarGitLab Duo <gitlab-duo@gitlab.com>
Co-authored-by: Hercules Merscher's avatarHercules Merscher <hmerscher@gitlab.com>
parents 5fb4ccfb 067eaf69
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
require 'net/http'
require 'uri'
require 'json'
require 'active_support/core_ext/module/attribute_accessors'

module Labkit
  module JsonSchema
+29 −1
Original line number Diff line number Diff line
# frozen_string_literal: true

require 'labkit/json_schema/ref_resolver'
require 'labkit/logging/json_logger'
require 'labkit/user_experience_sli/current'
require 'labkit/user_experience_sli/error'
require 'labkit/user_experience_sli/experience'
require 'labkit/user_experience_sli/null'
require 'labkit/user_experience_sli/registry'
require 'labkit/logging/json_logger'

module Labkit
  # Labkit::UserExperienceSli namespace module.
@@ -23,6 +24,33 @@ module Labkit
        @registry_path = File.join("config", "user_experience_slis")
        @ref_resolver_timeout = 2
      end

      def feature_category_schema_path=(path)
        preload_feature_category_schema(path) if path
      end

      private

      def preload_feature_category_schema(path)
        internal_schema = JSON.parse(File.read(Registry::SCHEMA_PATH))
        ref_url = internal_schema.dig("properties", "feature_category", "$ref")
        return unless ref_url

        schema = parse_schema_json(read_schema_file(path), path)
        Labkit::JsonSchema::RefResolver.cache[ref_url] = schema
      end

      def read_schema_file(path)
        File.read(path)
      rescue StandardError => e
        raise(UserExperienceError, "Failed to read feature category schema file at '#{path}': #{e.message}")
      end

      def parse_schema_json(content, path)
        JSON.parse(content)
      rescue JSON::ParserError => e
        raise(UserExperienceError, "Failed to parse feature category schema JSON at '#{path}': #{e.message}")
      end
    end

    class << self
+27 −0
Original line number Diff line number Diff line
@@ -50,6 +50,33 @@ This configuration is useful when:

**Note:** The timeout applies to both connection opening and reading operations when fetching remote JSON schema references.

### Feature Category Schema Preloading

When validating user experience SLI definitions, the system validates the `feature_category` field against a remote up to date JSON schema (cached after first request) by default. To avoid HTTP requests at runtime, you can preload the feature category schema from a local file:

```ruby
Labkit::UserExperienceSli.configure do |config|
  config.feature_category_schema_path = Rails.root.join('config/feature_categories/schema.json').to_s
end
```

JSON schema example:

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "string",
  "enum": ["source_code_management", "team_planning", "observability"]
}
```

The current single source of truth can be found in https://gitlab.com/gitlab-org/gitlab/-/raw/master/config/feature_categories/schema.json.


This configuration is useful when:
- Running in environments without external network access
- Ensuring consistent validation against a known schema version

### User Experience Definitions

User Experience SLI definitions will be lazy loaded from the default directory (`config/user_experience_slis`).
+5 −0
Original line number Diff line number Diff line
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "string",
  "enum": ["source_code_management", "team_planning", "observability"]
}
+50 −0
Original line number Diff line number Diff line
@@ -116,6 +116,56 @@ RAILS_ENVIRONMENTS = %w[test development].freeze
          expect(configuration.ref_resolver_timeout).to eq(5)
        end
      end

      describe "#feature_category_schema_path=" do
        let(:fixtures_dir) { File.join(__dir__, "../fixtures") }
        let(:schema_path) { File.join(fixtures_dir, "feature_category_schema.json") }
        let(:expected_ref_url) { "https://gitlab.com/gitlab-org/gitlab/-/raw/master/config/feature_categories/schema.json" }

        before do
          Labkit::JsonSchema::RefResolver.cache.clear
        end

        it "preloads schema into RefResolver cache using internal schema $ref as key" do
          configuration.feature_category_schema_path = schema_path

          expect(Labkit::JsonSchema::RefResolver.cache).to have_key(expected_ref_url)
          expect(Labkit::JsonSchema::RefResolver.cache[expected_ref_url]).to include("type" => "string")
        end

        it "does nothing when path is nil" do
          configuration.feature_category_schema_path = nil

          expect(Labkit::JsonSchema::RefResolver.cache).to be_empty
        end

        context "when file cannot be read" do
          it "raises a friendly error for non-existent file" do
            expect do
              configuration.feature_category_schema_path = "/nonexistent/path/schema.json"
            end.to raise_error(
              Labkit::UserExperienceSli::UserExperienceError,
              %r{Failed to read feature category schema file at '/nonexistent/path/schema\.json'}
            )
          end
        end

        context "when JSON is invalid" do
          it "raises a friendly error for invalid JSON" do
            expect do
              Tempfile.create(["invalid_json_schema", ".json"]) do |file|
                file.write("not valid json {")
                file.flush

                configuration.feature_category_schema_path = file.path
              end
            end.to raise_error(
              Labkit::UserExperienceSli::UserExperienceError,
              /Failed to parse feature category schema JSON at '.*invalid_json_schema.*\.json'/
            )
          end
        end
      end
    end

    describe '.registry' do