Skip secrets_stored billing events for opted-out namespaces

What does this MR do and why?

When a root namespace opts out of the GitLab Secrets Manager, secret writes are blocked, so its stored-secret counts stay above zero indefinitely. The daily billing job still processes every root namespace with a non-zero count, which meant opted-out namespaces kept emitting a daily secrets_stored billing event forever and kept being billed.

This MR adds a guard to the billing event emitter so it returns early when the root namespace has opted out. All emissions go through this emitter, so the guard covers every path, and it runs before the secret-count query, so opted-out namespaces skip that query as well. Two new specs verify that an opted-out namespace emits no billing event and that a namespace with an active enrollment still emits, so the guard only applies to the disabled state.

References

Screenshots or screen recordings

Not applicable — backend-only change with no UI.

How to set up and validate locally

In a rails console:

  1. Enable the feature flag:

    Feature.enable(:secrets_manager_emit_secret_stored_events)
  2. Pick a root group and seed a secret count row:

    group = Group.find_by_full_path('<your-root-group>')
    SecretsManagement::NamespaceSecretCount.upsert_all([{ namespace_id: group.id, root_namespace_id: group.id, count: 5 }], unique_by: :namespace_id)
  3. Opt the group out:

    SecretsManagement::NamespaceEnrollment.create!(namespace: group, disabled_at: Time.current)
  4. Run the emitter and confirm no billing event is tracked:

    SecretsManagement::BillableEvents::SecretsStoredEmitter.new(group).emit!

    It returns early and Gitlab::BillingEvents::Client.track_billing_event is not called. Before this fix, it emitted a secrets_stored event with quantity 5.

  5. Remove disabled_at and re-run to confirm the event is emitted again:

    SecretsManagement::NamespaceEnrollment.find_by(namespace_id: group.id).update!(disabled_at: nil)
    SecretsManagement::BillableEvents::SecretsStoredEmitter.new(group).emit!

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Merge request reports

Loading
Loading