Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/release-id-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ruby': minor
---

Read the release id from the `POSTHOG_RELEASE_ID` environment variable and send it as `$release_id` on every event, including `$exception`, `$identify`, `$groupidentify`, `$create_alias` and minimal `$feature_flag_called` events. On `$exception` events, error tracking uses it to link the exception to its release by a direct id lookup. Create the release and get its id with `posthog-cli release resolve`. The client reads the variable once, when it is created. An explicit `$release_id` in the event properties or the request context wins over the environment variable, and `before_send` can still change or remove it.
21 changes: 21 additions & 0 deletions lib/posthog/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@ class Client
$is_server
$lib
$lib_version
$release_id
].flat_map { |key| [key, key.to_sym] }.freeze
private_constant :MINIMAL_FLAG_CALLED_EVENT_PROPERTIES

# Holds the id that `posthog-cli release resolve` prints. Error tracking
# links an exception to its release by this id, because a Ruby app has no
# build step that could inject it.
RELEASE_ID_ENV_VAR = 'POSTHOG_RELEASE_ID'
private_constant :RELEASE_ID_ENV_VAR

# Thread-safe tracking of client instances per API key for singleton warnings
@instances_by_api_key = {}
@instances_mutex = Mutex.new
Expand Down Expand Up @@ -217,6 +224,7 @@ def initialize(opts = {})

@before_send = opts[:before_send]
@is_server = opts.fetch(:is_server, true) != false
@release_id = normalize_string_option(ENV.fetch(RELEASE_ID_ENV_VAR, nil), blank_as_nil: true)
@deprecation_emitted_for = Concurrent::Set.new
end

Expand Down Expand Up @@ -1137,6 +1145,8 @@ def process_before_send(action)
def enqueue(action)
return false if @disabled || shutdown?

# Every event type passes through here, and before_send still sees the id.
action = add_release_id(action)
action = process_before_send(action)
return false if action.nil? || action.empty?

Expand Down Expand Up @@ -1168,6 +1178,17 @@ def enqueue(action)
end
end

# A `$release_id` the caller or the request context already set wins.
def add_release_id(action)
return action if @release_id.nil?

properties = action[:properties]
return action unless properties.is_a?(Hash)
return action if properties.key?('$release_id') || properties.key?(:$release_id)

action.merge(properties: properties.merge('$release_id' => @release_id))
end

def normalize_string_option(value, blank_as_nil: false)
return value unless value.is_a?(String)

Expand Down
82 changes: 82 additions & 0 deletions spec/posthog/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,88 @@ def release
end
end

describe 'release id from POSTHOG_RELEASE_ID' do
let(:release_id) { '01928f3c-6f1d-7c5e-9a1b-2c3d4e5f6a7b' }
let(:release_id_env) { release_id }
let(:client) { Client.new(api_key: API_KEY, test_mode: true) }

before do
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with('POSTHOG_RELEASE_ID', nil).and_return(release_id_env)
end

{
'capture' => ->(c) { c.capture(distinct_id: 'user', event: 'Event') },
'capture_exception' => ->(c) { c.capture_exception(StandardError.new('boom'), 'user') },
'identify' => ->(c) { c.identify(distinct_id: 'user', properties: { 'plan' => 'pro' }) },
'group_identify' => ->(c) { c.group_identify(group_type: 'company', group_key: 'id:5') },
'alias' => ->(c) { c.alias(distinct_id: 'user', alias: 'anon') }
}.each do |method_name, call|
it "sends $release_id on #{method_name} events" do
call.call(client)

expect(client.dequeue_last_message[:properties]['$release_id']).to eq(release_id)
end
end

it 'keeps $release_id out of the person and group properties' do
client.identify(distinct_id: 'user', properties: { 'plan' => 'pro' })
expect(client.dequeue_last_message[:$set]).to eq('plan' => 'pro')

client.group_identify(group_type: 'company', group_key: 'id:5', properties: { 'name' => 'Acme' })
expect(client.dequeue_last_message[:properties][:$group_set]).to eq('name' => 'Acme')
end

[
['a string key', { '$release_id' => 'explicit' }, '$release_id'],
['a symbol key', { '$release_id': 'explicit' }, :$release_id]
].each do |description, properties, key|
it "keeps an explicit $release_id passed with #{description}" do
client.capture(distinct_id: 'user', event: 'Event', properties: properties)

sent = client.dequeue_last_message[:properties]
expect(sent[key]).to eq('explicit')
expect(sent.keys.count { |k| k.to_s == '$release_id' }).to eq(1)
end
end

it 'passes $release_id to before_send, which can remove it' do
client = Client.new(
api_key: API_KEY,
test_mode: true,
before_send: lambda do |event|
event[:properties].delete('$release_id')
event
end
)
client.capture(distinct_id: 'user', event: 'Event')

expect(client.dequeue_last_message[:properties]).not_to have_key('$release_id')
end

context 'when the value has surrounding whitespace' do
let(:release_id_env) { " #{release_id}\n" }

it 'trims it' do
client.capture(distinct_id: 'user', event: 'Event')

expect(client.dequeue_last_message[:properties]['$release_id']).to eq(release_id)
end
end

[nil, '', " \n"].each do |value|
context "when the variable is #{value.inspect}" do
let(:release_id_env) { value }

it 'omits $release_id' do
client.capture(distinct_id: 'user', event: 'Event')

expect(client.dequeue_last_message[:properties]).not_to have_key('$release_id')
end
end
end
end

context 'common' do
let(:message_id) { '123e4567-e89b-12d3-a456-426614174000' }

Expand Down
9 changes: 9 additions & 0 deletions spec/posthog/feature_flag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5549,6 +5549,15 @@ def flag_called_properties(client, key, groups: {})
expect(properties['$groups']).to eq(company: 'id_5')
end

it 'keeps $release_id from POSTHOG_RELEASE_ID on minimal events' do
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with('POSTHOG_RELEASE_ID', nil).and_return('release-abc')
stub_definitions(local_definitions)
c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)

expect(flag_called_properties(c, 'test-flag')['$release_id']).to eq('release-abc')
end

it 'sends the full event when the flag has an experiment' do
stub_definitions(local_definitions(has_experiment: true))
c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)
Expand Down
Loading