From 64a7524abb7a3aeb64078c8334da15b3a9786f71 Mon Sep 17 00:00:00 2001 From: Niko Belokolodov <nbelokolodov@gitlab.com> Date: Wed, 24 Jul 2024 22:04:19 +0000 Subject: [PATCH] Allow more than 3 additional properties in Internal Events --- config/events/schema.json | 2 +- lib/api/helpers.rb | 2 +- lib/api/usage_data.rb | 11 +-- lib/gitlab/internal_events.rb | 32 ++++--- lib/gitlab/tracking/event_definition.rb | 16 ---- .../tracking/event_definition_validator.rb | 60 +++++++++++++ scripts/internal_events/monitor.rb | 9 +- scripts/verify-tff-mapping | 6 +- spec/lib/gitlab/internal_events_spec.rb | 34 +++++--- .../gitlab/tracking/event_definition_spec.rb | 27 ------ .../event_definition_validator_spec.rb | 85 +++++++++++++++++++ ...definition_validator_validate_all_spec.rb} | 2 +- spec/requests/api/usage_data_spec.rb | 3 +- .../internal_event_tracking_examples.rb | 7 +- tests.yml | 2 +- 15 files changed, 205 insertions(+), 93 deletions(-) create mode 100644 lib/gitlab/tracking/event_definition_validator.rb create mode 100644 spec/lib/gitlab/tracking/event_definition_validator_spec.rb rename spec/lib/gitlab/tracking/{event_definition_validate_all_spec.rb => event_definition_validator_validate_all_spec.rb} (77%) diff --git a/config/events/schema.json b/config/events/schema.json index 8b0f89fbd1b44..eb1c37d332c70 100644 --- a/config/events/schema.json +++ b/config/events/schema.json @@ -86,7 +86,7 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": true }, "iglu_schema_url": { "type": [ diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a5ec50cc9b694..421cdba499e54 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -719,7 +719,7 @@ def increment_unique_values(event_name, values) Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") end - def track_event(event_name, user:, send_snowplow_event: true, namespace_id: nil, project_id: nil, additional_properties: Gitlab::InternalEvents::DEFAULT_ADDITIONAL_PROPERTIES) + def track_event(event_name, user:, send_snowplow_event: true, namespace_id: nil, project_id: nil, additional_properties: {}) return unless user.present? namespace = Namespace.find(namespace_id) if namespace_id diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index c157545304c03..f5c3df17df587 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -95,29 +95,26 @@ class UsageData < ::API::Base documentation: { example: 1234 } optional :project_id, type: Integer, desc: 'Project ID', documentation: { example: 1234 } + optional :additional_properties, type: Hash, desc: 'Additional properties to be tracked', + documentation: { example: { label: 'login_button', value: 1 } } end post 'track_event', urgency: :low do event_name = params[:event] namespace_id = params[:namespace_id] project_id = params[:project_id] - additional_properties = params - .fetch(:additional_properties, Gitlab::InternalEvents::DEFAULT_ADDITIONAL_PROPERTIES) - .symbolize_keys + additional_properties = params.fetch(:additional_properties, {}).symbolize_keys unless Gitlab::Tracking::AiTracking.track_via_code_suggestions?(event_name, current_user) Gitlab::Tracking::AiTracking.track_event(event_name, additional_properties.merge(user: current_user)) end - internal_event_additional_props = additional_properties - .slice(*Gitlab::InternalEvents::ALLOWED_ADDITIONAL_PROPERTIES.keys) - track_event( event_name, send_snowplow_event: false, user: current_user, namespace_id: namespace_id, project_id: project_id, - additional_properties: internal_event_additional_props + additional_properties: additional_properties ) status :ok diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb index 25616406fbe87..c18b82460d8e6 100644 --- a/lib/gitlab/internal_events.rb +++ b/lib/gitlab/internal_events.rb @@ -8,12 +8,11 @@ module InternalEvents SNOWPLOW_EMITTER_BUFFER_SIZE = 100 DEFAULT_BUFFER_SIZE = 1 - ALLOWED_ADDITIONAL_PROPERTIES = { + BASE_ADDITIONAL_PROPERTIES = { label: [String], property: [String], value: [Integer, Float] }.freeze - DEFAULT_ADDITIONAL_PROPERTIES = {}.freeze KEY_EXPIRY_LENGTH = Gitlab::UsageDataCounters::HLLRedisCounter::KEY_EXPIRY_LENGTH class << self @@ -23,7 +22,10 @@ class << self def track_event( event_name, category: nil, send_snowplow_event: true, - additional_properties: DEFAULT_ADDITIONAL_PROPERTIES, **kwargs) + additional_properties: {}, **kwargs) + + extra = custom_additional_properties(additional_properties) + additional_properties = additional_properties.slice(*BASE_ADDITIONAL_PROPERTIES.keys) unless Gitlab::Tracking::EventDefinition.internal_event_exists?(event_name) raise UnknownEventError, "Unknown event: #{event_name}" @@ -35,7 +37,7 @@ def track_event( kwargs[:namespace] ||= project.namespace if project update_redis_values(event_name, additional_properties, kwargs) - trigger_snowplow_event(event_name, category, additional_properties, kwargs) if send_snowplow_event + trigger_snowplow_event(event_name, category, additional_properties, extra, kwargs) if send_snowplow_event send_application_instrumentation_event(event_name, additional_properties, kwargs) if send_snowplow_event if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip) @@ -73,17 +75,8 @@ def validate_property!(hash, key, *class_names) end def validate_additional_properties!(additional_properties) - return if additional_properties.empty? - - disallowed_properties = additional_properties.keys - ALLOWED_ADDITIONAL_PROPERTIES.keys - unless disallowed_properties.empty? - info = "Additional properties should include only #{ALLOWED_ADDITIONAL_PROPERTIES.keys}. " \ - "Disallowed properties found: #{disallowed_properties}" - raise InvalidPropertyError, info - end - - additional_properties.each do |key, _value| - allowed_classes = ALLOWED_ADDITIONAL_PROPERTIES[key] + BASE_ADDITIONAL_PROPERTIES.keys.intersection(additional_properties.keys).each do |key| + allowed_classes = BASE_ADDITIONAL_PROPERTIES[key] validate_property!(additional_properties, key, *allowed_classes) end end @@ -106,6 +99,10 @@ def update_redis_values(event_name, additional_properties, kwargs) end end + def custom_additional_properties(additional_properties) + additional_properties.except(*BASE_ADDITIONAL_PROPERTIES.keys) + end + def update_total_counter(event_selection_rule) expiry = event_selection_rule.time_framed? ? KEY_EXPIRY_LENGTH : nil @@ -132,7 +129,7 @@ def update_unique_counter(event_selection_rule, kwargs) ) end - def trigger_snowplow_event(event_name, category, additional_properties, kwargs) + def trigger_snowplow_event(event_name, category, additional_properties, extra, kwargs) user = kwargs[:user] project = kwargs[:project] namespace = kwargs[:namespace] @@ -143,7 +140,8 @@ def trigger_snowplow_event(event_name, category, additional_properties, kwargs) user_id: user&.id, namespace_id: namespace&.id, plan_name: namespace&.actual_plan_name, - feature_enabled_by_namespace_ids: feature_enabled_by_namespace_ids + feature_enabled_by_namespace_ids: feature_enabled_by_namespace_ids, + **extra ).to_context service_ping_context = Tracking::ServicePingContext.new( diff --git a/lib/gitlab/tracking/event_definition.rb b/lib/gitlab/tracking/event_definition.rb index e8a6556e4a774..50beba6433c81 100644 --- a/lib/gitlab/tracking/event_definition.rb +++ b/lib/gitlab/tracking/event_definition.rb @@ -5,9 +5,6 @@ module Tracking InvalidEventError = Class.new(RuntimeError) class EventDefinition - EVENT_SCHEMA_PATH = Rails.root.join('config', 'events', 'schema.json') - SCHEMA = ::JSONSchemer.schema(EVENT_SCHEMA_PATH) - attr_reader :path attr_reader :attributes @@ -65,19 +62,6 @@ def yaml_path path.delete_prefix(Rails.root.to_s) end - def validation_errors - SCHEMA.validate(attributes.deep_stringify_keys).map do |error| - <<~ERROR_MSG - --------------- VALIDATION ERROR --------------- - Definition file: #{path} - Error type: #{error['type']} - Data: #{error['data']} - Path: #{error['data_pointer']} - Details: #{error['details']} - ERROR_MSG - end - end - def event_selection_rules @event_selection_rules ||= find_event_selection_rules end diff --git a/lib/gitlab/tracking/event_definition_validator.rb b/lib/gitlab/tracking/event_definition_validator.rb new file mode 100644 index 0000000000000..ce952987c501b --- /dev/null +++ b/lib/gitlab/tracking/event_definition_validator.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Gitlab + module Tracking + class EventDefinitionValidator + EVENT_SCHEMA_PATH = Rails.root.join('config/events/schema.json') + SCHEMA = ::JSONSchemer.schema(EVENT_SCHEMA_PATH) + + def initialize(definition) + @attributes = definition.attributes + @path = definition.path + end + + def validation_errors + (validate_schema << validate_additional_properties).compact + end + + private + + attr_reader :attributes, :path + + def validate_additional_properties + additional_props = attributes[:additional_properties]&.map { |k, _| k } + + return unless additional_props.present? + + extra_props = additional_props - main_additional_properties + + return unless extra_props.count == additional_props.count + + # if additional properties were not used + + <<~ERROR_MSG + --------------- VALIDATION ERROR --------------- + Definition file: #{path} + Error type: consider using the built-in additional properties: + "#{main_additional_properties.join(', ')}" + before adding custom extra properties: #{extra_props.join(', ')} + ERROR_MSG + end + + def main_additional_properties + Gitlab::InternalEvents::BASE_ADDITIONAL_PROPERTIES.keys - [:value] + end + + def validate_schema + SCHEMA.validate(attributes.deep_stringify_keys).map do |error| + <<~ERROR_MSG + --------------- VALIDATION ERROR --------------- + Definition file: #{path} + Error type: #{error['type']} + Data: #{error['data']} + Path: #{error['data_pointer']} + Details: #{error['details']} + ERROR_MSG + end + end + end + end +end diff --git a/scripts/internal_events/monitor.rb b/scripts/internal_events/monitor.rb index a1a455436e133..83ea3edf539e6 100644 --- a/scripts/internal_events/monitor.rb +++ b/scripts/internal_events/monitor.rb @@ -54,7 +54,8 @@ def extract_standard_context(event) user_id: context["data"]["user_id"], namespace_id: context["data"]["namespace_id"], project_id: context["data"]["project_id"], - plan: context["data"]["plan"] + plan: context["data"]["plan"], + extra: context["data"]["extra"] } end {} @@ -75,7 +76,8 @@ def generate_snowplow_table 'plan', 'Label', 'Property', - 'Value' + 'Value', + 'Extra' ] rows << :separator @@ -93,7 +95,8 @@ def generate_snowplow_table standard_context[:plan], event['event']['se_label'], event['event']['se_property'], - event['event']['se_value'] + event['event']['se_value'], + standard_context[:extra] ] row.map! { |value| red(value) } if event['rawEvent']['parameters']['dtm'].to_i > @initial_max_timestamp diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping index 772011ab30127..6e3b81a349a6a 100755 --- a/scripts/verify-tff-mapping +++ b/scripts/verify-tff-mapping @@ -100,17 +100,17 @@ tests = [ { explanation: 'FOSS Snowplow definitions map to event validation spec', changed_file: 'config/events/status_page_incident_unpublished.yml', - expected: ['spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb'] + expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'] }, { explanation: 'EE Snowplow definitions map to event validation spec', changed_file: 'ee/config/events/licenses_list_viewed.yml', - expected: ['spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb'] + expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'] }, { explanation: 'Snowplow schema maps to event validation spec', changed_file: 'config/events/schema.json', - expected: ['spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb'] + expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'] }, { diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb index c85dd8f638aba..1b8c855028ed3 100644 --- a/spec/lib/gitlab/internal_events_spec.rb +++ b/spec/lib/gitlab/internal_events_spec.rb @@ -60,7 +60,7 @@ def expect_redis_tracking end end - def expect_snowplow_tracking(expected_namespace = nil, expected_additional_properties = {}) + def expect_snowplow_tracking(expected_namespace = nil, expected_additional_properties = {}, extra: {}) service_ping_context = Gitlab::Tracking::ServicePingContext .new(data_source: :redis_hll, event: event_name) .to_context @@ -81,7 +81,7 @@ def expect_snowplow_tracking(expected_namespace = nil, expected_additional_prope c[:schema] == Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL end - validate_standard_context(standard_context, expected_namespace) + validate_standard_context(standard_context, expected_namespace, extra) # Verify Service Ping context service_ping_context = contexts.find { |c| c[:schema] == Gitlab::Tracking::ServicePingContext::SCHEMA_URL } @@ -90,12 +90,13 @@ def expect_snowplow_tracking(expected_namespace = nil, expected_additional_prope end end - def validate_standard_context(standard_context, expected_namespace) + def validate_standard_context(standard_context, expected_namespace, extra) namespace = expected_namespace || project&.namespace expect(standard_context).not_to eq(nil) expect(standard_context[:data][:user_id]).to eq(user&.id) expect(standard_context[:data][:namespace_id]).to eq(namespace&.id) expect(standard_context[:data][:project_id]).to eq(project&.id) + expect(standard_context[:data][:extra]).to eq(extra) end def validate_service_ping_context(service_ping_context) @@ -192,6 +193,23 @@ def validate_service_ping_context(service_ping_context) expect_snowplow_tracking(nil, additional_properties) end + context 'with a custom property' do + let(:custom_properties) do + additional_properties.merge(custom: 'custom_property') + end + + it 'is sent to Snowplow' do + described_class.track_event( + event_name, + additional_properties: custom_properties, + user: user, + project: project + ) + + expect_snowplow_tracking(nil, additional_properties, extra: { custom: 'custom_property' }) + end + end + context 'when a filter is defined' do let(:time_framed) { true } let(:event_selection_rules) do @@ -377,16 +395,6 @@ def validate_service_ping_context(service_ping_context) end end end - - context "when disallowed additional properties are passed" do - let(:error_class) { described_class::InvalidPropertyError } - let(:additional_properties) { { new_property: 'value' } } - - it_behaves_like 'an event that logs an error' do - let(:event_kwargs) { { user: user } } - let(:logged_kwargs) { { user: user.id } } - end - end end it 'updates Redis, RedisHLL and Snowplow', :aggregate_failures do diff --git a/spec/lib/gitlab/tracking/event_definition_spec.rb b/spec/lib/gitlab/tracking/event_definition_spec.rb index 1fee6ac2451b1..7a7a913a39396 100644 --- a/spec/lib/gitlab/tracking/event_definition_spec.rb +++ b/spec/lib/gitlab/tracking/event_definition_spec.rb @@ -76,33 +76,6 @@ def write_metric(metric, path, content) end end - describe '#validate' do - using RSpec::Parameterized::TableSyntax - - where(:attribute, :value) do - :description | 1 - :category | nil - :action | nil - :label_description | 1 - :property_description | 1 - :value_description | 1 - :extra_properties | 'smth' - :product_group | nil - :distributions | %(be eb) - :tiers | %(pro) - end - - with_them do - before do - attributes[attribute] = value - end - - it 'has validation errors' do - expect(described_class.new(path, attributes).validation_errors).not_to be_empty - end - end - end - describe '.definitions' do let(:metric1) { Dir.mktmpdir('metric1') } let(:metric2) { Dir.mktmpdir('metric2') } diff --git a/spec/lib/gitlab/tracking/event_definition_validator_spec.rb b/spec/lib/gitlab/tracking/event_definition_validator_spec.rb new file mode 100644 index 0000000000000..12946ab1770e2 --- /dev/null +++ b/spec/lib/gitlab/tracking/event_definition_validator_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Tracking::EventDefinitionValidator, feature_category: :service_ping do + let(:attributes) do + { + description: 'Created issues', + category: 'issues', + action: 'create', + label_description: 'API', + property_description: 'The string "issue_id"', + value_description: 'ID of the issue', + extra_properties: { confidential: false }, + product_group: 'group::product analytics', + distributions: %w[ee ce], + tiers: %w[free premium ultimate], + introduced_by_url: "https://gitlab.com/example/-/merge_requests/123", + milestone: '1.6' + } + end + + let(:path) { File.join('events', 'issues_create.yml') } + let(:definition) { Gitlab::Tracking::EventDefinition.new(path, attributes) } + # let(:yaml_content) { attributes.deep_stringify_keys.to_yaml } + + describe '#validate' do + using RSpec::Parameterized::TableSyntax + + where(:attribute, :value) do + :description | 1 + :category | nil + :action | nil + :label_description | 1 + :property_description | 1 + :value_description | 1 + :extra_properties | 'smth' + :product_group | nil + :distributions | %(be eb) + :tiers | %(pro) + end + + with_them do + before do + attributes[attribute] = value + end + + it 'has validation errors' do + expect(described_class.new(definition).validation_errors).not_to be_empty + end + end + + describe 'internal event additional_properties' do + let(:attributes) do + { + description: 'Created issues', + category: 'issues', + action: 'create', + internal_events: true, + product_group: 'activation', + introduced_by_url: "https://gitlab.com/example/-/merge_requests/123", + distributions: %w[ce], + milestone: "1.0", + tiers: %w[free], + additional_properties: { + label: { description: "desc" }, + property: { description: "desc" } + } + } + end + + it 'valid when extra added in addition to the built in' do + attributes[:additional_properties][:extra] = { description: "desc" } + + expect(described_class.new(definition).validation_errors).to be_empty + end + + it 'invalid when extra added with no built in used' do + attributes[:additional_properties] = { extra: { description: "desc" } } + + expect(described_class.new(definition).validation_errors).not_to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb b/spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb similarity index 77% rename from spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb rename to spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb index cc2ccc511bb38..c39cd9f0af4f1 100644 --- a/spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb +++ b/spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Gitlab::Tracking::EventDefinition, feature_category: :product_analytics_data_management do it 'only has valid event definitions', :aggregate_failures do described_class.definitions.each do |definition| - validation_errors = definition.validation_errors + validation_errors = Gitlab::Tracking::EventDefinitionValidator.new(definition).validation_errors expect(validation_errors).to be_empty, validation_errors.join end end diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb index 9ddd79b2db0d1..8a5619195fd5e 100644 --- a/spec/requests/api/usage_data_spec.rb +++ b/spec/requests/api/usage_data_spec.rb @@ -208,7 +208,8 @@ let_it_be(:additional_properties) do { label: 'label3', - property: 'admin' + property: 'admin', + lang: 'ruby' } end diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb index 0c808d1ebde73..7fb134d4345dc 100644 --- a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb @@ -11,11 +11,13 @@ # - label # - property # - value +# - additional_properties # - event_attribute_overrides RSpec.shared_examples 'internal event tracking' do let(:all_metrics) do - additional_properties = Gitlab::InternalEvents::ALLOWED_ADDITIONAL_PROPERTIES.to_h do |key, _val| + additional_properties = try(:additional_properties) || {} + base_additional_properties = Gitlab::InternalEvents::BASE_ADDITIONAL_PROPERTIES.to_h do |key, _val| [key, try(key)] end @@ -26,7 +28,8 @@ # Only include unique metrics if the unique_identifier_name is present in the spec next if event_selection_rule.unique_identifier_name && !try(event_selection_rule.unique_identifier_name) - event_selection_rule.matches?(additional_properties) + properties = additional_properties.merge(base_additional_properties) + event_selection_rule.matches?(properties) end definition.key if matching_rules.flatten.any? diff --git a/tests.yml b/tests.yml index 24533a92f720e..0e76f9a8ac61e 100644 --- a/tests.yml +++ b/tests.yml @@ -53,7 +53,7 @@ mapping: - source: - '(?<prefix>ee/)?config/events/.+\.yml' - 'config/events/schema\.json' - test: 'spec/lib/gitlab/tracking/event_definition_validate_all_spec.rb' + test: 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb' # DB structure should map to schema spec - source: 'db/structure\.sql' -- GitLab