Skip to content

Quick start for Internal Event Tracking

In an effort to provide a more efficient, scalable, and unified tracking API, GitLab is deprecating existing RedisHLL and Snowplow tracking. Instead, we're implementing a new track_event (Backend) and trackEvent(Frontend) method. With this approach, we can update both RedisHLL counters and send Snowplow events without worrying about the underlying implementation.

In order to instrument your code with Internal Events Tracking you need to do three things:

  1. Define an event
  2. Define one or more metrics
  3. Trigger the event

Defining event and metrics

To create event and/or metric definitions, use the internal_events generator from the gitlab directory:

scripts/internal_events/cli.rb

This CLI will help you create the correct definition files based on your specific use-case, then provide code examples for instrumentation and testing.

Events should be named in the format of <action>_<target_of_action>_<where/when>, valid examples are create_ci_build or click_previous_blame_on_blob_page.

Trigger events

Triggering an event and thereby updating a metric is slightly different on backend and frontend. Refer to the relevant section below.

Backend tracking

To trigger an event, call the track_internal_event method from the Gitlab::InternalEventsTracking module with the desired arguments:

include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)

This method automatically increments all RedisHLL metrics relating to the event create_ci_build, and sends a corresponding Snowplow event with all named arguments and standard context (SaaS only). In addition, the name of the class triggering the event is saved in the category property of the Snowplow event.

If you have defined a metric with a unique property such as unique: project.id it is required that you provide the project argument.

It is encouraged to fill out as many of user, namespace and project as possible as it increases the data quality and make it easier to define metrics in the future.

If a project but no namespace is provided, the project.namespace is used as the namespace for the event.

In some cases you might want to specify the category manually or provide none at all. To do that, you can call the InternalEvents.track_event method directly instead of using the module.

In case when a feature is enabled through multiple namespaces and its required to track why the feature is enabled, it is possible to pass an optional feature_enabled_by_namespace_ids parameter with an array of namespace ids.

track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)

Additional properties

Additional properties can be passed when tracking events. They can be used to save additional data related to given event.

Tracking classes already have three built-in properties:

  • label (string)
  • property (string)
  • value(numeric)

The arbitrary naming and typing of the these three properties is due to constraints from the data extraction process. It's recommended to use these properties first, even if their name does not match with the data you want to track. You can further describe what is the actual data being tracked by using the description property in the YAML definition of the event. For an example, see create_ci_internal_pipeline.yml:

additional_properties:
  label:
    description: The source of the pipeline, e.g. a push, a schedule or similar.
  property:
    description: The source of the config, e.g. the repository, auto_devops or similar.

Additional properties are passed by including the additional_properties hash in the #track_event call:

track_internal_event(
  "create_ci_build",
  user: user,
  additional_properties: {
    label: source, # The label is tracking the source of the pipeline
    property: config_source # The property is tracking the source of the configuration
  }
)

If you need to pass more than the three built-in additional properties, you can use the additional_properties hash with your custom keys:

track_internal_event(
  "code_suggestion_accepted",
  user: user,
  additional_properties: {
    # Built-in properties
    label: editor_name,
    property: suggestion_type,
    value: suggestion_shown_duration,
    # Your custom properties
    lang: 'ruby',
    custom_key: 'custom_value'
  }
)

Please add custom properties only in addition to the built-in properties. Additional properties can only have string or numeric values.

Controller and API helpers

There is a helper module ProductAnalyticsTracking for controllers you can use to track internal events for particular controller actions by calling #track_internal_event:

class Projects::PipelinesController < Projects::ApplicationController
  include ProductAnalyticsTracking

  track_internal_event :charts, name: 'visit_charts_on_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? }

  def charts
    ...
  end

  private

  def should_track_ci_cd_pipelines?
    params[:chart].blank? || params[:chart] == 'pipelines'
  end
end

You need to add these two methods to the controller body, so that the helper can get the current project and namespace for the event:

  private

  def tracking_namespace_source
    project.namespace
  end

  def tracking_project_source
    project
  end

Also, there is an API helper:

track_event(
  event_name,
  user: current_user,
  namespace_id: namespace_id,
  project_id: project_id
)

Batching

When multiple events are emitted at once, use with_batched_redis_writes to batch all of them in a single Redis call.

Gitlab::InternalEvents.with_batched_redis_writes do
  incr.times { Gitlab::InternalEvents.track_event(event) }
end

Notice that only updates to total counters are batched. If n unique metrics and m total counter metrics are defined, it will result in incr * n + m Redis writes.

Backend testing

When testing code that simply triggers an internal event and make sure it increments all the related metrics, you can use the internal_event_tracking shared example.

include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```0

It requires a context containing:

- `subject` - the action that triggers the event
- `event` - the name of the event

Optionally, the context can contain:

- `user`
- `project`
- `namespace`. If not provided, `project.namespace` will be used (if `project` is available).
- `category`
- `additional_properties`
- `event_attribute_overrides` - is used when its necessary to override the attributes available in parent context. For example:

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```1

These legacy options are now deprecated:

- `label`
- `property`
- `value`

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.

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```2

The `trigger_internal_events` matcher accepts the same chain methods as the [`receive`](https://rubydoc.info/github/rspec/rspec-mocks/RSpec/Mocks/ExampleMethods#receive-instance_method) matcher (`#once`, `#at_most`, etc). By default, it expects the provided events to be triggered only once.

The chain method `#with` accepts following parameters:

- `user` - User object
- `project` - Project object
- `namespace` - Namespace object. If not provided, it will be set to `project.namespace`
- `additional_properties` - Hash. Additional properties to be sent with the event. For example: `{ label: 'scheduled', value: 20 }`
- `category` - String. If not provided, it will be set to the class name of the object that triggers the event

The `increment_usage_metrics` matcher accepts the same chain methods as the [`change`](https://rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:change) matcher (`#by`, `#from`, `#to`, etc). By default, it expects the provided metrics to be incremented by one.

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```3

Both matchers are composable with other matchers that act on a block (like `change` matcher).

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```4

To test that an event was not triggered, you can use the `not_trigger_internal_events` matcher. It does not accept message chains.

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```5

Or you can use the `not_to` syntax:

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```6

### Frontend tracking

Any frontend tracking call automatically passes the values `user.id`, `namespace.id`, and `project.id` from the current context of the page.

If you need to pass any further properties, such as `extra`, `context`, `label`, `property`, and `value`, you can use the [deprecated snowplow implementation](https://archives.docs.gitlab.com/16.4/ee/development/internal_analytics/snowplow/implementation.html). In this case, let us know about your specific use-case in our [feedback issue for Internal Events](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/internal/-/issues/690).

#### Vue components

In Vue components, tracking can be done with [Vue mixin](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/tracking/internal_events.js#L29).

To implement Vue component tracking:

1. Import the `InternalEvents` library and call the `mixin` method:

     ```javascript
     import { InternalEvents } from '~/tracking';
     const trackingMixin = InternalEvents.mixin();
    ```

1. Use the mixin in the component:

   ```javascript
   export default {
     mixins: [trackingMixin],

     data() {
       return {
         expanded: false,
       };
     },
   };
  1. Call the trackEvent method. Tracking options can be passed as the second parameter:

    this.trackEvent('click_previous_blame_on_blob_page');

    Or use the trackEvent method in the template:

    <template>
      <div>
        <button data-testid="toggle" @click="toggle">Toggle</button>
    
        <div v-if="expanded">
          <p>Hello world!</p>
          <button @click="trackEvent('click_previous_blame_on_blob_page')">Track another event</button>
        </div>
      </div>
    </template>

Raw JavaScript

For tracking events directly from arbitrary frontend JavaScript code, a module for raw JavaScript is provided. This can be used outside of a component context where the Mixin cannot be utilized.

include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```7

#### Data-event attribute

This attribute ensures that if we want to track GitLab internal events for a button, we do not need to write JavaScript code on Click handler. Instead, we can just add a data-event-tracking attribute with event value and it should work. This can also be used with HAML views.

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```8

#### Haml

```ruby
include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)
```9

#### Internal events on render

Sometimes we want to send internal events when the component is rendered or loaded. In these cases, we can add the `data-event-tracking-load="true"` attribute:

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```0

#### Additional properties

You can include additional properties with events to save additional data. When included you must define each additional property in the `additional_properties` field. It is possible to send the three built-in additional properties with keys `label` (string), `property` (string) and `value`(numeric) and [custom additional properties](quick_start.md#additional-properties) if the built-in properties are not sufficient.

NOTE:
Do not pass the page URL or page path as an additional property because we already track the pseudonymized page URL for each event.
Getting the URL from `window.location` does not pseudonymize project and namespace information [as documented](https://metrics.gitlab.com/identifiers).

For Vue Mixin:

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```1

For raw JavaScript:

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```2

For data-event attributes:

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```3

For Haml:

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```4

#### Frontend testing

##### JavaScript/Vue

If you are using the `trackEvent` method in any of your code, whether it is in raw JavaScript or a Vue component, you can use the `useMockInternalEventsTracking` helper method to assert if `trackEvent` is called.

For example, if we need to test the below Vue component,

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```5

Below would be the test case for above component.

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```6

If you are using tracking attributes for in Vue/View templates like below,

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```7

Below would be the test case for above component.

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```8

#### 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.

For example, if we need to test the below Haml,

```ruby
track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)
```9

Below would be the test case for above haml.

- [RSpec view specs](https://rspec.info/features/6-0/rspec-rails/view-specs/view-spec/)

```ruby
additional_properties:
  label:
    description: The source of the pipeline, e.g. a push, a schedule or similar.
  property:
    description: The source of the config, e.g. the repository, auto_devops or similar.
```0

- [ViewComponent](https://viewcomponent.org/) specs

```ruby
additional_properties:
  label:
    description: The source of the pipeline, e.g. a push, a schedule or similar.
  property:
    description: The source of the config, e.g. the repository, auto_devops or similar.
```1

`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.

### Using Internal Events API

You can also use our API to track events from other systems connected to a GitLab instance.
See the [Usage Data API documentation](../../../api/usage_data.md#events-tracking-api) for more information.

### Internal Events on other systems

Apart from the GitLab codebase, we are using Internal Events for the systems listed below.

1. [AI gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/internal_events.md?ref_type=heads)
1. [Switchboard](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/switchboard/-/blob/main/docs/internal_events.md)