diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js index e94ba86e12a2cfba67b212dd6cc607d76b9b407c..b90302c92bb93244aeb533983dcad7e7aafaf794 100644 --- a/app/assets/javascripts/tracking/constants.js +++ b/app/assets/javascripts/tracking/constants.js @@ -22,6 +22,7 @@ export const DEFAULT_SNOWPLOW_OPTIONS = { export const ACTION_ATTR_SELECTOR = '[data-track-action]'; export const LOAD_ACTION_ATTR_SELECTOR = '[data-track-action="render"]'; +// Keep these in sync with the strings used in spec/support/matchers/internal_events_matchers.rb export const INTERNAL_EVENTS_SELECTOR = '[data-event-tracking]'; export const LOAD_INTERNAL_EVENTS_SELECTOR = '[data-event-tracking-load="true"]'; diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md index 72c4f938d0d96f876995b75ce6527fac7aac38e2..319cdda910f83a02b3b107198b78c0429704b0e6 100644 --- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md +++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md @@ -239,7 +239,7 @@ Prefer using `additional_properties` instead. #### Composable matchers When a singe action triggers an event multiple times, triggers multiple different events, or increments some metrics but not others for the event, -you can use the `trigger_internal_events` and `increment_usage_metrics` matchers. +you can use the `trigger_internal_events` and `increment_usage_metrics` matchers on a block argument. ```ruby expect { subject } @@ -293,6 +293,8 @@ Or you can use the `not_to` syntax: expect { subject }.not_to trigger_internal_events('mr_created', 'member_role_created') ``` +The `trigger_internal_events` matcher can also be used for testing [Haml with data attributes](#haml-with-data-attributes). + ### Frontend tracking Any frontend tracking call automatically passes the values `user.id`, `namespace.id`, and `project.id` from the current context of the page. @@ -579,40 +581,65 @@ describe('DeleteApplication', () => { #### Haml with data attributes -If you are using the data attributes to register tracking at the Haml layer, -you can use the `have_internal_tracking` matcher method to assert if expected data attributes are assigned. +If you are using [data attributes](#data-event-attribute) to track internal events at the Haml layer, +you can use the [`trigger_internal_events` matcher](#composable-matchers) to assert that the expected properties are present. -For example, if we need to test the below Haml, +For example, if you need to test the below Haml, ```haml -%div{ data: { testid: '_testid_', event_tracking: 'render', event_label: '_tracking_label_' } } +%div{ data: { testid: '_testid_', event_tracking: 'some_event', event_label: 'some_label' } } ``` +You can call assertions on any rendered HTML compatible with the `have_css` matcher. +Use the `:on_click` and `:on_load` chain methods to indicate when you expect the event to trigger. + Below would be the test case for above haml. -- [RSpec view specs](https://rspec.info/features/6-0/rspec-rails/view-specs/view-spec/) +- rendered HTML is a `String` ([RSpec views](https://rspec.info/features/6-0/rspec-rails/view-specs/view-spec/)) ```ruby it 'assigns the tracking items' do render - expect(rendered).to have_internal_tracking(event: 'render', label: '_tracking_label_', testid: '_testid_') + expect(rendered).to trigger_internal_events('some_event').on_click + .with(additional_properties: { label: 'some_label' }) end ``` -- [ViewComponent](https://viewcomponent.org/) specs +- rendered HTML is a `Capybara::Node::Simple` ([ViewComponent](https://viewcomponent.org/)) + +```ruby + it 'assigns the tracking items' do + render_inline(component) + + expect(page.find_by_testid('_testid_')) + .to trigger_internal_events('some_event').on_click + .with(additional_properties: { label: 'some_label' }) + end +``` + +- rendered HTML is a `Nokogiri::HTML4::DocumentFragment` ([ViewComponent](https://viewcomponent.org/)) + +```ruby + it 'assigns the tracking items' do + expect(render_inline(component)) + .to trigger_internal_events('some_event').on_click + .with(additional_properties: { label: 'some_label' }) + end +``` + +Or you can use the `not_to` syntax: ```ruby it 'assigns the tracking items' do render_inline(component) - expect(page).to have_internal_tracking(event: 'render', label: '_tracking_label_', testid: '_testid_') + expect(page).not_to trigger_internal_events end ``` -`event` is required for the matcher and `label`/`testid` are optional. -It is recommended to use `testid` when possible for exactness. -When you want to ensure that tracking isn't assigned, you can use `not_to` with the above matchers. +When negated, the matcher accepts no additional chain methods or arguments. +This asserts that no tracking attributes are in use. ### Using Internal Events API diff --git a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/free_component_spec.rb b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/free_component_spec.rb index 381d0f09f81686aad57716addd4bd23a0b2f74b3..d43adecdfe2b9df4635f036399267a0f0031e88f 100644 --- a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/free_component_spec.rb +++ b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/free_component_spec.rb @@ -48,13 +48,16 @@ end it 'has the primary action' do + expected_link = new_trial_path(namespace_id: namespace.id) + is_expected.to have_link( 'Start free trial of GitLab Ultimate and GitLab Duo Enterprise', - href: new_trial_path(namespace_id: namespace.id) + href: expected_link ) - attributes = { event: 'click_duo_enterprise_trial_billing_page', label: 'ultimate_and_duo_enterprise_trial' } - is_expected.to have_internal_tracking(attributes) + expect(component.find(:link, href: expected_link)) + .to trigger_internal_events('click_duo_enterprise_trial_billing_page').on_click + .with(additional_properties: { label: 'ultimate_and_duo_enterprise_trial' }) end it 'has the hand raise lead selector' do diff --git a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/premium_component_spec.rb b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/premium_component_spec.rb index 7800e50e94f3f5c5faf056a686c007b4755c0e30..78ae52fee6c59bb3df28356ca309c53fcf6432d5 100644 --- a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/premium_component_spec.rb +++ b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/premium_component_spec.rb @@ -38,13 +38,16 @@ shared_examples 'has the primary action' do it 'has the action' do + expected_link = new_trial_path(namespace_id: namespace.id) + is_expected.to have_link( 'Start free trial of GitLab Ultimate and GitLab Duo Enterprise', - href: new_trial_path(namespace_id: namespace.id) + href: expected_link ) - attributes = { event: 'click_duo_enterprise_trial_billing_page', label: 'ultimate_and_duo_enterprise_trial' } - is_expected.to have_internal_tracking(attributes) + expect(component.find(:link, href: expected_link)) + .to trigger_internal_events('click_duo_enterprise_trial_billing_page').on_click + .with(additional_properties: { label: 'ultimate_and_duo_enterprise_trial' }) end end @@ -84,12 +87,13 @@ it { is_expected.to have_content(duo_pro_text) } it 'has the secondary action' do - is_expected.to have_link( - 'Try GitLab Duo Pro', href: new_trials_duo_pro_path(namespace_id: namespace.id) - ) + expected_link = new_trials_duo_pro_path(namespace_id: namespace.id) + + is_expected.to have_link('Try GitLab Duo Pro', href: expected_link) - attributes = { event: 'click_duo_enterprise_trial_billing_page', label: 'duo_pro_trial' } - is_expected.to have_internal_tracking(attributes) + expect(component.find(:link, href: expected_link)) + .to trigger_internal_events('click_duo_enterprise_trial_billing_page').on_click + .with(additional_properties: { label: 'duo_pro_trial' }) end end end diff --git a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/ultimate_component_spec.rb b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/ultimate_component_spec.rb index 18b78ec169766906544ace875ceff745b7189522..d56ffeeb13b4009a42fdc5d89e35accf93b59fc9 100644 --- a/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/ultimate_component_spec.rb +++ b/ee/spec/components/gitlab_subscriptions/duo_enterprise_alert/ultimate_component_spec.rb @@ -47,13 +47,16 @@ end it 'has the primary action' do + expected_link = new_trials_duo_enterprise_path(namespace_id: namespace.id) + is_expected.to have_link( 'Start a free GitLab Duo Enterprise Trial', - href: new_trials_duo_enterprise_path(namespace_id: namespace.id) + href: expected_link ) - attributes = { event: 'click_duo_enterprise_trial_billing_page', label: 'duo_enterprise_trial' } - is_expected.to have_internal_tracking(attributes) + expect(component.find(:link, href: expected_link)) + .to trigger_internal_events('click_duo_enterprise_trial_billing_page').on_click + .with(additional_properties: { label: 'duo_enterprise_trial' }) end it 'has the hand raise lead selector' do diff --git a/spec/support/matchers/have_tracking.rb b/spec/support/matchers/have_tracking.rb index aeff7ce429f36f85cdaf832e0e3eeb4f14adfe5c..e257bec7aaeca4ed129f408dfb4d5fdec9b24715 100644 --- a/spec/support/matchers/have_tracking.rb +++ b/spec/support/matchers/have_tracking.rb @@ -12,13 +12,3 @@ expect(rendered).to have_css(css) end end - -RSpec::Matchers.define :have_internal_tracking do |event:, label: nil, testid: nil| - match do |rendered| - css = "[data-event-tracking='#{event}']" - css += "[data-event-label='#{label}']" if label - css += "[data-testid='#{testid}']" if testid - - expect(rendered).to have_css(css) - end -end diff --git a/spec/support/matchers/internal_events_matchers.rb b/spec/support/matchers/internal_events_matchers.rb index 742069982d84c43b783d26e574d100fd2de4d0ea..c0a4c104c5c649a341ac51c6a2c0fe7371647a24 100644 --- a/spec/support/matchers/internal_events_matchers.rb +++ b/spec/support/matchers/internal_events_matchers.rb @@ -14,7 +14,7 @@ # Example: # expect { subject } # .to trigger_internal_events('web_ide_viewed') -# .with(user: user, project: project, namespace: namepsace) +# .with(user: user, project: project, namespace: namespace) # # -- #increment_usage_metrics ------- # Use: Asserts that one or more usage metric was incremented by the right value. @@ -96,6 +96,10 @@ def apply_chain_methods(base_matcher, chained_methods) RSpec::Matchers.define :trigger_internal_events do |*event_names| include InternalEventsMatchHelpers + def supports_value_expectations? + true + end + description { "trigger the internal events: #{event_names.join(', ')}" } failure_message { @failure_message } @@ -117,22 +121,41 @@ def apply_chain_methods(base_matcher, chained_methods) end end - match do |proc| - @event_names = event_names.flatten - @properties ||= {} - @chained_methods ||= [[:once]] + chain(:on_click) { @on_click = true } + chain(:on_load) { @on_load = true } + match do |input| + setup_match_context(event_names) check_if_params_provided!(:events, @event_names) check_if_events_exist!(@event_names) + input.is_a?(Proc) ? expect_events_to_fire(input) : expect_data_attributes(input) + end + + match_when_negated do |input| + setup_match_context(event_names) + check_if_events_exist!(@event_names) + + input.is_a?(Proc) ? expect_no_events_to_fire(input) : expect_data_attributes(input, negate: true) + end + + private + + def setup_match_context(event_names) + @event_names = event_names.flatten + @properties ||= {} + end + + def expect_no_events_to_fire(proc) + # rubocop:disable RSpec/ExpectGitlabTracking -- Supersedes the #expect_snowplow_event helper for internal events + allow(Gitlab::Tracking).to receive(:event).and_call_original allow(Gitlab::InternalEvents).to receive(:track_event).and_call_original - allow(Gitlab::Redis::HLL).to receive(:add).and_call_original + # rubocop:enable RSpec/ExpectGitlabTracking collect_expectations do |event_name| [ - expect_internal_event(event_name), - expect_snowplow(event_name), - expect_product_analytics(event_name) + expect_no_snowplow_event(event_name), + expect_no_internal_event(event_name) ] end @@ -149,20 +172,19 @@ def apply_chain_methods(base_matcher, chained_methods) unstub_expectations end - match_when_negated do |proc| - @event_names = event_names.flatten + def expect_events_to_fire(proc) + check_chain_methods_for_block! - check_if_events_exist!(@event_names) + @chained_methods ||= [[:once]] - # rubocop:disable RSpec/ExpectGitlabTracking -- Supercedes the #expect_snowplow_event helper for internal events - allow(Gitlab::Tracking).to receive(:event).and_call_original allow(Gitlab::InternalEvents).to receive(:track_event).and_call_original - # rubocop:enable RSpec/ExpectGitlabTracking + allow(Gitlab::Redis::HLL).to receive(:add).and_call_original collect_expectations do |event_name| [ - expect_no_snowplow_event(event_name), - expect_no_internal_event(event_name) + expect_internal_event(event_name), + expect_snowplow(event_name), + expect_product_analytics(event_name) ] end @@ -179,7 +201,37 @@ def apply_chain_methods(base_matcher, chained_methods) unstub_expectations end - private + # All `node` inputs should be compatible with the have_css matcher + # https://www.rubydoc.info/gems/capybara/Capybara/RSpecMatchers#have_css-instance_method + def expect_data_attributes(node, negate: false) + # ensure assertions work for Capybara::Node::Simple inputs + node = node.native if node.respond_to?(:native) + + check_negated_chain_methods_for_node! if negate + check_chain_methods_for_node! + check_negated_events_limit_for_node! if negate + check_events_limit_for_node! + + expect_data_attribute(node, 'tracking', @event_names.first) + expect_data_attribute(node, 'label', @additional_properties.try(:[], :label)) + expect_data_attribute(node, 'property', @additional_properties.try(:[], :property)) + expect_data_attribute(node, 'value', @additional_properties.try(:[], :value)) + expect_data_attribute(node, 'tracking-load', @on_load) + + true + rescue RSpec::Expectations::ExpectationNotMetError => e + @failure_message = e.message + false + end + + # Keep this in sync with the constants in app/assets/javascripts/tracking/constants.js + def expect_data_attribute(node, attribute, value) + if value + expect(node).to have_css("[data-event-#{attribute}=\"#{value}\"]") + else + expect(node).not_to have_css("[data-event-#{attribute}]") + end + end def receive_expected_count_of(message) apply_chain_methods(receive(message), @chained_methods) @@ -302,6 +354,39 @@ def unstub_expectations doubled_module.expectations.pop end end + + def check_chain_methods_for_block! + return unless instance_variable_defined?(:@on_load) || instance_variable_defined?(:@on_click) + + raise ArgumentError, "Chain methods :on_click, :on_load are only available for Capybara::Node::Simple type " \ + "arguments" + end + + def check_events_limit_for_node! + return if @event_names.length <= 1 + + raise ArgumentError, "Providing multiple event names to #{name} is only supported for block arguments" + end + + def check_negated_events_limit_for_node! + return if @event_names.none? + + raise ArgumentError, "Negated #{name} matcher accepts no arguments or chain methods when testing data attributes" + end + + def check_chain_methods_for_node! + return unless @chained_methods + + raise ArgumentError, "Chain methods #{@chained_methods.map(&:first).join(',')} are only available for " \ + "block arguments" + end + + def check_negated_chain_methods_for_node! + return unless instance_variable_defined?(:@on_load) || instance_variable_defined?(:@on_click) || @properties.any? + + raise ArgumentError, "Chain methods :on_click, :on_load, :with are unavailable for negated #{name} matcher with " \ + "for Capybara::Node::Simple type arguments" + end end RSpec::Matchers.define :increment_usage_metrics do |*key_paths| diff --git a/spec/support_specs/matchers/internal_events_matchers_spec.rb b/spec/support_specs/matchers/internal_events_matchers_spec.rb index 6cd7f037df2af90c817a9732b4e0f1e186591c66..a32b4008c1efe5a7952cbb2f31caeb328c01f8fc 100644 --- a/spec/support_specs/matchers/internal_events_matchers_spec.rb +++ b/spec/support_specs/matchers/internal_events_matchers_spec.rb @@ -34,122 +34,240 @@ def track_event(event: nil, user: nil, group: nil) end describe ':trigger_internal_events' do - it 'raises error if no events are passed to :trigger_internal_events' do - expect do - expect { nil }.to trigger_internal_events - end.to raise_error ArgumentError, 'trigger_internal_events matcher requires events argument' - end + context 'when testing HTML with data attributes', type: :component do + using RSpec::Parameterized::TableSyntax + + let(:event_name) { 'g_edit_by_sfe' } + let(:label) { 'some_label' } + let(:rendered_html) do + <<-HTML + <div> + <a href="#" data-event-tracking="#{event_name}">Click me</a> + </div> + HTML + end - it 'does not raises error if no events are passed to :not_trigger_internal_events' do - expect do - expect { nil }.to not_trigger_internal_events - end.not_to raise_error - end + let(:capybara_node) { Capybara::Node::Simple.new(rendered_html) } - it_behaves_like 'matcher and negated matcher both raise expected error', - [:trigger_internal_events, 'bad_event_name'], - "Unknown event 'bad_event_name'! trigger_internal_events matcher accepts only existing events" + where(:html_input) do + [ + ref(:rendered_html), + ref(:capybara_node) + ] + end - it 'bubbles up failure messages' do - expect do - expect { nil }.to trigger_internal_events('g_edit_by_sfe') - end.to raise_expectation_error_with <<~TEXT - (Gitlab::InternalEvents).track_event("g_edit_by_sfe", *(any args)) - expected: 1 time with arguments: ("g_edit_by_sfe", *(any args)) - received: 0 times - TEXT - end + with_them do + context 'when using positive matcher' do + it 'matches elements with correct tracking attribute' do + expect(html_input).to trigger_internal_events(event_name).on_click + end + + context 'with incorrect tracking attribute' do + let(:event_name) { 'wrong_event' } + + it 'does not match elements' do + expect do + expect(html_input).to trigger_internal_events('g_edit_by_sfe') + end.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + context 'with non existing tracking event' do + let(:event_name) { 'wrong_event' } + + it 'does not match elements' do + expect do + expect(html_input).to trigger_internal_events(event_name) + end.to raise_error(ArgumentError) + end + end + + context 'with additional properties' do + let(:rendered_html) do + <<-HTML + <div> + <a href="#" data-event-tracking="#{event_name}" data-event-label=\"#{label}\">Click me</a> + </div> + HTML + end + + it 'matches elements' do + expect(html_input).to trigger_internal_events(event_name).with(additional_properties: { label: label }) + end + end + + context 'with tracking-load attribute' do + let(:rendered_html) do + <<-HTML + <div> + <a href="#" data-event-tracking="#{event_name}" data-event-tracking-load=\"true\">Click me</a> + </div> + HTML + end + + it 'matches elements' do + expect(rendered_html).to trigger_internal_events(event_name).on_load + end + end + + it 'raises error when multiple events are provided' do + expect do + expect(rendered_html).to trigger_internal_events(event_name, event_name) + end.to raise_error(ArgumentError, /Providing multiple event names.*is only supported for block arguments/) + end + + it 'raises error when using incompatible chain methods' do + expect do + expect(rendered_html).to trigger_internal_events(event_name).once + end.to raise_error(ArgumentError, /Chain methods.*are only available for block arguments/) + end + end - it 'bubbles up failure messages for negated matcher' do - expect do - expect { track_event }.not_to trigger_internal_events('g_edit_by_sfe') - end.to raise_expectation_error_with <<~TEXT - (Gitlab::InternalEvents).track_event("g_edit_by_sfe", {:namespace=>#<Group id:#{group_1.id} @#{group_1.name}>, :user=>#<User id:#{user_1.id} @#{user_1.username}>}) - expected: 0 times with arguments: ("g_edit_by_sfe", anything) - received: 1 time with arguments: ("g_edit_by_sfe", {:namespace=>#<Group id:#{group_1.id} @#{group_1.name}>, :user=>#<User id:#{user_1.id} @#{user_1.username}>}) - TEXT + context 'when using negated matcher' do + let(:rendered_html) { '<div></div>' } + + it 'matches elements without tracking attribute' do + expect(rendered_html).not_to trigger_internal_events + end + + it 'raises error when passing events to negated matcher' do + expect do + expect(rendered_html).not_to trigger_internal_events(event_name) + end.to raise_error(ArgumentError, /Negated trigger_internal_events matcher accepts no arguments/) + end + + it 'raises error when using chain methods with negated matcher' do + expect do + expect(rendered_html).not_to trigger_internal_events(event_name) + .with(additional_properties: { label: label }) + end.to raise_error(ArgumentError, /Chain methods.*are unavailable for negated.*matcher/) + end + end + end end - it 'handles events that should not be triggered' do - expect { track_event }.to not_trigger_internal_events('web_ide_viewed') - end + context 'with backend events' do + it 'raises error if no events are passed to :trigger_internal_events' do + expect do + expect { nil }.to trigger_internal_events + end.to raise_error ArgumentError, 'trigger_internal_events matcher requires events argument' + end - it 'ignores extra/irrelevant triggered events' do - expect do - # web_ide_viewed event should not cause a failure when we're only testing g_edit_by_sfe - Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_1, namespace: group_1) - Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) - end.to trigger_internal_events('g_edit_by_sfe') - end + it 'does not raises error if no events are passed to :not_trigger_internal_events' do + expect do + expect { nil }.to not_trigger_internal_events + end.not_to raise_error + end - it 'accepts chained event counts like #receive for multiple different events' do - expect do - # #track_event and #trigger_internal_events should be order independent - Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) - Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_2, namespace: group_2) - Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_2, namespace: group_2) - Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_2, namespace: group_2) - Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) - end.to trigger_internal_events('g_edit_by_sfe') - .with(user: user_1, namespace: group_1) - .at_least(:once) - .and trigger_internal_events('web_ide_viewed') - .with(user: user_2, namespace: group_2) - .exactly(2).times - .and trigger_internal_events('g_edit_by_sfe') - .with(user: user_2, namespace: group_2) - .once - end + it_behaves_like 'matcher and negated matcher both raise expected error', + [:trigger_internal_events, 'bad_event_name'], + "Unknown event 'bad_event_name'! trigger_internal_events matcher accepts only existing events" - context 'with additional properties' do - let(:additional_properties) { { label: 'label1', value: 123, property: 'property1' } } - let(:tracked_params) { { user: user_1, namespace: group_1, additional_properties: additional_properties } } - let(:expected_params) { tracked_params } + it 'bubbles up failure messages' do + expect do + expect { nil }.to trigger_internal_events('g_edit_by_sfe') + end.to raise_expectation_error_with <<~TEXT + (Gitlab::InternalEvents).track_event("g_edit_by_sfe", *(any args)) + expected: 1 time with arguments: ("g_edit_by_sfe", *(any args)) + received: 0 times + TEXT + end - subject(:assertion) do + it 'bubbles up failure messages for negated matcher' do + expect do + expect { track_event }.not_to trigger_internal_events('g_edit_by_sfe') + end.to raise_expectation_error_with <<~TEXT + (Gitlab::InternalEvents).track_event("g_edit_by_sfe", {:namespace=>#<Group id:#{group_1.id} @#{group_1.name}>, :user=>#<User id:#{user_1.id} @#{user_1.username}>}) + expected: 0 times with arguments: ("g_edit_by_sfe", anything) + received: 1 time with arguments: ("g_edit_by_sfe", {:namespace=>#<Group id:#{group_1.id} @#{group_1.name}>, :user=>#<User id:#{user_1.id} @#{user_1.username}>}) + TEXT + end + + it 'handles events that should not be triggered' do + expect { track_event }.to not_trigger_internal_events('web_ide_viewed') + end + + it 'ignores extra/irrelevant triggered events' do + expect do + # web_ide_viewed event should not cause a failure when we're only testing g_edit_by_sfe + Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_1, namespace: group_1) + Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) + end.to trigger_internal_events('g_edit_by_sfe') + end + + it 'accepts chained event counts like #receive for multiple different events' do expect do - Gitlab::InternalEvents.track_event('g_edit_by_sfe', **tracked_params) + # #track_event and #trigger_internal_events should be order independent + Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) + Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_2, namespace: group_2) + Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_2, namespace: group_2) + Gitlab::InternalEvents.track_event('web_ide_viewed', user: user_2, namespace: group_2) + Gitlab::InternalEvents.track_event('g_edit_by_sfe', user: user_1, namespace: group_1) end.to trigger_internal_events('g_edit_by_sfe') - .with(expected_params) + .with(user: user_1, namespace: group_1) + .at_least(:once) + .and trigger_internal_events('web_ide_viewed') + .with(user: user_2, namespace: group_2) + .exactly(2).times + .and trigger_internal_events('g_edit_by_sfe') + .with(user: user_2, namespace: group_2) .once end - shared_examples 'raises error for unexpected event args' do - specify do - expect { assertion }.to raise_error RSpec::Expectations::ExpectationNotMetError, - /received :event with unexpected arguments/ + context 'with additional properties' do + let(:extra_track_params) { {} } + let(:additional_properties) { { label: 'label1', value: 123, property: 'property1' } } + let(:tracked_params) do + { user: user_1, namespace: group_1, additional_properties: additional_properties.merge(extra_track_params) } end - end - it 'accepts correct additional properties' do - assertion - end + let(:expected_params) { tracked_params } + + subject(:assertion) do + expect do + Gitlab::InternalEvents.track_event('g_edit_by_sfe', **tracked_params) + end.to trigger_internal_events('g_edit_by_sfe') + .with(expected_params) + .once + end - context 'with extra attributes' do - let(:tracked_params) { super().deep_merge(additional_properties: { other_property: 'other_prop' }) } + shared_examples 'raises error for unexpected event args' do + specify do + expect { assertion }.to raise_error RSpec::Expectations::ExpectationNotMetError, + /received :event with unexpected arguments/ + end + end - it 'accepts correct extra attributes' do + it 'accepts correct additional properties' do assertion end - end - context "with wrong label value" do - let(:expected_params) { tracked_params.deep_merge(additional_properties: { label: 'wrong_label' }) } + context 'with extra attributes' do + let(:extra_track_params) { { other_property: 'other_prop' } } - it_behaves_like 'raises error for unexpected event args' - end + it 'accepts correct extra attributes' do + assertion + end + end - context 'with extra attributes expected but not tracked' do - let(:expected_params) { tracked_params.deep_merge(additional_properties: { other_property: 'other_prop' }) } + context "with wrong label value" do + let(:expected_params) { tracked_params.deep_merge(additional_properties: { label: 'wrong_label' }) } - it_behaves_like 'raises error for unexpected event args' - end + it_behaves_like 'raises error for unexpected event args' + end - context 'with extra attributes tracked but not expected' do - let(:expected_params) { { user: user_1, namespace: group_1, additional_properties: additional_properties } } - let(:tracked_params) { expected_params.deep_merge(additional_properties: { other_property: 'other_prop' }) } + context 'with extra attributes expected but not tracked' do + let(:expected_params) { tracked_params.deep_merge(additional_properties: { other_property: 'other_prop' }) } - it_behaves_like 'raises error for unexpected event args' + it_behaves_like 'raises error for unexpected event args' + end + + context 'with extra attributes tracked but not expected' do + let(:expected_params) { { user: user_1, namespace: group_1, additional_properties: additional_properties } } + let(:tracked_params) { expected_params.deep_merge(additional_properties: { other_property: 'other_prop' }) } + + it_behaves_like 'raises error for unexpected event args' + end end end end diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb index 10d2ce59edcf976690afdf099dafeaf1eeea2c2a..a75a27f507a07a7db8557bc326b11785b20f2195 100644 --- a/spec/views/search/_results.html.haml_spec.rb +++ b/spec/views/search/_results.html.haml_spec.rb @@ -90,7 +90,9 @@ it 'renders the click text event tracking attributes' do render - expect(rendered).to have_internal_tracking(event: 'click_search_result', label: scope) + expect(rendered) + .to trigger_internal_events('click_search_result').on_click + .with(additional_properties: { label: scope, value: 1 }) end end @@ -98,7 +100,7 @@ it 'does not render the click text event tracking attributes' do render - expect(rendered).not_to have_internal_tracking(event: 'click_search_result', label: scope) + expect(rendered).not_to trigger_internal_events end end end @@ -134,7 +136,9 @@ it 'renders the click text event tracking attributes' do render - expect(rendered).to have_internal_tracking(event: 'click_search_result', label: scope) + expect(rendered) + .to trigger_internal_events('click_search_result').on_click + .with(additional_properties: { label: scope, value: 1 }) end end @@ -142,7 +146,7 @@ it 'does not render the click text event tracking attributes' do render - expect(rendered).not_to have_internal_tracking(event: 'click_search_result', label: scope) + expect(rendered).not_to trigger_internal_events end end