From 30d51002c0ffed8f99f782b2c7f0a02a1aaf782e Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 12:55:57 +0200 Subject: [PATCH 1/2] fix: generate UUID v7 identifiers --- .changeset/quiet-rabbits-build.md | 5 ++++ lib/posthog/client.rb | 4 ++-- lib/posthog/field_parser.rb | 5 ++-- lib/posthog/uuid.rb | 38 +++++++++++++++++++++++++++++++ spec/posthog/client_spec.rb | 16 ++++++------- 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 .changeset/quiet-rabbits-build.md create mode 100644 lib/posthog/uuid.rb diff --git a/.changeset/quiet-rabbits-build.md b/.changeset/quiet-rabbits-build.md new file mode 100644 index 0000000..d0a3bcc --- /dev/null +++ b/.changeset/quiet-rabbits-build.md @@ -0,0 +1,5 @@ +--- +'posthog-ruby': patch +--- + +Generate SDK-created event and personless identifiers as UUID v7. diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 5f80feb..4683769 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -2,7 +2,6 @@ require 'time' require 'json' -require 'securerandom' require 'posthog/defaults' require 'posthog/logging' @@ -16,6 +15,7 @@ require 'posthog/send_feature_flags_options' require 'posthog/exception_capture' require 'posthog/internal/context' +require 'posthog/uuid' module PostHog class Client @@ -810,7 +810,7 @@ def enrich_capture_attrs_with_context(attrs) return end - attrs[:distinct_id] = SecureRandom.uuid + attrs[:distinct_id] = PostHog::Uuid.v7 return unless properties_are_hash return if property_key?(explicit_properties, '$process_person_profile') diff --git a/lib/posthog/field_parser.rb b/lib/posthog/field_parser.rb index 880952a..cf0e60c 100644 --- a/lib/posthog/field_parser.rb +++ b/lib/posthog/field_parser.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require 'securerandom' - require 'posthog/logging' +require 'posthog/uuid' module PostHog # Converts public SDK method arguments into PostHog API event payloads. @@ -196,7 +195,7 @@ def normalized_uuid(fields) message_id = fields[:message_id] return message_id if message_id && valid_uuid_for_event_props?(message_id) - SecureRandom.uuid + PostHog::Uuid.v7 end # @param [Object] uuid - the UUID to validate, user provided, so we don't know the type diff --git a/lib/posthog/uuid.rb b/lib/posthog/uuid.rb new file mode 100644 index 0000000..1db4697 --- /dev/null +++ b/lib/posthog/uuid.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'securerandom' + +module PostHog + # UUID generation helpers used by SDK-generated identifiers. + # + # @api private + module Uuid + module_function + + def v7 + return SecureRandom.uuid_v7 if SecureRandom.respond_to?(:uuid_v7) + + bytes = uuid_v7_bytes + hex = bytes.pack('C*').unpack1('H*') + "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}" + end + + def uuid_v7_bytes + timestamp_ms = (Time.now.to_f * 1000).to_i & 0xffffffffffff + bytes = [ + (timestamp_ms >> 40) & 0xff, + (timestamp_ms >> 32) & 0xff, + (timestamp_ms >> 24) & 0xff, + (timestamp_ms >> 16) & 0xff, + (timestamp_ms >> 8) & 0xff, + timestamp_ms & 0xff, + *SecureRandom.bytes(10).bytes + ] + + bytes[6] = (bytes[6] & 0x0f) | 0x70 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + bytes + end + private_class_method :uuid_v7_bytes + end +end diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index 3a4a609..65d91ea 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -4,6 +4,8 @@ module PostHog flags_endpoint = 'https://us.i.posthog.com/flags/?v=2' + UUID_V7_REGEX = + /\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil @@ -259,8 +261,7 @@ module PostHog client.capture(event: 'Event') message = client.dequeue_last_message - expect(message[:distinct_id]).to be_a(String) - expect(message[:distinct_id].length).to eq(36) + expect(message[:distinct_id]).to match(UUID_V7_REGEX) expect(message[:properties]['$process_person_profile']).to be false end @@ -771,7 +772,7 @@ module PostHog ) message = client.dequeue_last_message - expect(message['uuid']).to match(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i) + expect(message['uuid']).to match(UUID_V7_REGEX) expect(logger).to have_received(:warn).with( 'UUID is not valid: i am also not a uuid. Ignoring it.' ) @@ -788,7 +789,7 @@ module PostHog ) message = client.dequeue_last_message - expect(message['uuid']).to match(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i) + expect(message['uuid']).to match(UUID_V7_REGEX) expect(logger).not_to have_received(:warn) end @@ -802,7 +803,7 @@ module PostHog ) message = client.dequeue_last_message - expect(message['uuid']).to match(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i) + expect(message['uuid']).to match(UUID_V7_REGEX) expect(logger).to have_received(:warn).with( 'UUID is not valid: i am obviously not a uuid. Ignoring it.' ) @@ -1068,7 +1069,7 @@ module PostHog ) message = client.dequeue_last_message - expect(message['uuid']).to match(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i) + expect(message['uuid']).to match(UUID_V7_REGEX) expect(logger).to have_received(:warn).with( 'UUID is not a string. Ignoring it.' ) @@ -1793,8 +1794,7 @@ def run message = client.dequeue_last_message - expect(message[:distinct_id]).to be_a(String) - expect(message[:distinct_id].length).to eq(36) + expect(message[:distinct_id]).to match(UUID_V7_REGEX) expect(message[:properties]['$process_person_profile']).to be false end end From 512fc777f1c50fea1c1c0f02ded80f7773397682 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 14:00:37 +0200 Subject: [PATCH 2/2] address uuid v7 review feedback --- lib/posthog/uuid.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/posthog/uuid.rb b/lib/posthog/uuid.rb index 1db4697..f201ac8 100644 --- a/lib/posthog/uuid.rb +++ b/lib/posthog/uuid.rb @@ -7,10 +7,13 @@ module PostHog # # @api private module Uuid + NATIVE_UUID_V7 = SecureRandom.respond_to?(:uuid_v7) + private_constant :NATIVE_UUID_V7 + module_function def v7 - return SecureRandom.uuid_v7 if SecureRandom.respond_to?(:uuid_v7) + return SecureRandom.uuid_v7 if NATIVE_UUID_V7 bytes = uuid_v7_bytes hex = bytes.pack('C*').unpack1('H*')