posthog-ruby 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 88d2c921b1cdccea3548414fd9d7f95fb0a4d99f
4
+ data.tar.gz: 8167ab967256d9f487cc29ba9b937f7c96839793
5
+ SHA512:
6
+ metadata.gz: 8f4acbdd445e76ca520a1b821645ead4c1be6ddc3bc0f3e385b75cadbf80162911daa9b02887a6f08020c07e84edc095d94ce0ae1c670b3de8dc11c2c83f76e7
7
+ data.tar.gz: 07fe6c72ca3be46ed0b85cb32b7b740ddbed80dc65407ddf3e2e49606e03a90290db3800f491b79bc776fbd0cdf64f5ef21b1b49149774f99d28d7f03f14a3de
data/bin/posthog ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'posthog'
4
+ require 'rubygems'
5
+ require 'commander/import'
6
+ require 'time'
7
+ require 'json'
8
+
9
+ program :name, 'posthog'
10
+ program :version, '1.0.0'
11
+ program :description, 'PostHog API'
12
+
13
+ def json_hash(str)
14
+ if str
15
+ return JSON.parse(str)
16
+ end
17
+ end
18
+
19
+ command :capture do |c|
20
+ c.description = 'capture an event'
21
+
22
+ c.option '--api-key=<string>', String, 'The PostHog API Key'
23
+ c.option '--api-host=<url>', String, 'The PostHog API URL host part (scheme+domain)'
24
+ c.option '--distinct-id=<distinct_id>', String, 'The distinct id to send the event as'
25
+ c.option '--event=<event>', String, 'The event name to send with the event'
26
+ c.option '--properties=<properties>', 'The properties to send (JSON-encoded)'
27
+
28
+ c.action do |args, options|
29
+ posthog = PostHog::Client.new({
30
+ api_key: options.api_key,
31
+ api_host: options.api_host,
32
+ on_error: Proc.new { |status, msg| print msg }
33
+ })
34
+
35
+ posthog.capture({
36
+ distinct_id: options.distinct_id,
37
+ event: options.event,
38
+ properties: json_hash(options.properties)
39
+ })
40
+
41
+ posthog.flush
42
+ end
43
+ end
44
+
45
+ command :identify do |c|
46
+ c.description = 'identify the user'
47
+
48
+ c.option '--api-key=<api_key>', String, 'The PostHog API Key'
49
+ c.option '--api-host=<url>', String, 'The PostHog API URL host part (scheme+domain)'
50
+ c.option '--distinct-id=<distinct_id>', String, 'The distinct id to send the event as'
51
+ c.option '--properties=<properties>', 'The properties to send (JSON-encoded)'
52
+
53
+ c.action do |args, options|
54
+ posthog = PostHog::Client.new({
55
+ api_key: options.api_key,
56
+ api_host: options.api_host,
57
+ on_error: Proc.new { |status, msg| print msg }
58
+ })
59
+
60
+ posthog.identify({
61
+ distinct_id: options.distinct_id,
62
+ properties: json_hash(options.properties)
63
+ })
64
+
65
+ posthog.flush
66
+ end
67
+ end
68
+
69
+ command :alias do |c|
70
+ c.description = 'set an alias for a distinct id'
71
+
72
+ c.option '--api-key=<api_key>', String, 'The PostHog API Key'
73
+ c.option '--api-host=<url>', String, 'The PostHog API URL host part (scheme+domain)'
74
+ c.option '--distinct-id=<distinct_id>', String, 'The distinct id'
75
+ c.option '--alias=<alias>', 'The alias to give to the distinct id'
76
+
77
+ c.action do |args, options|
78
+ posthog = PostHog::Client.new({
79
+ api_key: options.api_key,
80
+ api_host: options.api_host,
81
+ on_error: Proc.new { |status, msg| print msg }
82
+ })
83
+
84
+ posthog.alias({
85
+ distinct_id: options.distinct_id,
86
+ alias: options.alias,
87
+ })
88
+
89
+ posthog.flush
90
+ end
91
+ end
@@ -0,0 +1,47 @@
1
+ require 'posthog/defaults'
2
+
3
+ class PostHog
4
+ class BackoffPolicy
5
+ include PostHog::Defaults::BackoffPolicy
6
+
7
+ # @param [Hash] opts
8
+ # @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
9
+ # @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
10
+ # @option opts [Numeric] :multiplier The value to multiply the current
11
+ # interval with for each retry attempt
12
+ # @option opts [Numeric] :randomization_factor The randomization factor
13
+ # to use to create a range around the retry interval
14
+ def initialize(opts = {})
15
+ @min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
16
+ @max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
17
+ @multiplier = opts[:multiplier] || MULTIPLIER
18
+ @randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
19
+
20
+ @attempts = 0
21
+ end
22
+
23
+ # @return [Numeric] the next backoff interval, in milliseconds.
24
+ def next_interval
25
+ interval = @min_timeout_ms * (@multiplier**@attempts)
26
+ interval = add_jitter(interval, @randomization_factor)
27
+
28
+ @attempts += 1
29
+
30
+ [interval, @max_timeout_ms].min
31
+ end
32
+
33
+ private
34
+
35
+ def add_jitter(base, randomization_factor)
36
+ random_number = rand
37
+ max_deviation = base * randomization_factor
38
+ deviation = random_number * max_deviation
39
+
40
+ if random_number < 0.5
41
+ base - deviation
42
+ else
43
+ base + deviation
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,134 @@
1
+ require 'thread'
2
+ require 'time'
3
+
4
+ require 'posthog/defaults'
5
+ require 'posthog/logging'
6
+ require 'posthog/utils'
7
+ require 'posthog/worker'
8
+
9
+ class PostHog
10
+ class Client
11
+ include PostHog::Utils
12
+ include PostHog::Logging
13
+
14
+ # @param [Hash] opts
15
+ # @option opts [String] :api_key Your project's api_key
16
+ # @option opts [FixNum] :max_queue_size Maximum number of calls to be
17
+ # remain queued.
18
+ # @option opts [Proc] :on_error Handles error calls from the API.
19
+ def initialize(opts = {})
20
+ symbolize_keys!(opts)
21
+
22
+ @queue = Queue.new
23
+ @api_key = opts[:api_key]
24
+ @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
25
+ @worker_mutex = Mutex.new
26
+ @worker = Worker.new(@queue, @api_key, opts)
27
+ @worker_thread = nil
28
+
29
+ check_api_key!
30
+
31
+ at_exit { @worker_thread && @worker_thread[:should_exit] = true }
32
+ end
33
+
34
+ # Synchronously waits until the worker has flushed the queue.
35
+ #
36
+ # Use only for scripts which are not long-running, and will specifically
37
+ # exit
38
+ def flush
39
+ while !@queue.empty? || @worker.is_requesting?
40
+ ensure_worker_running
41
+ sleep(0.1)
42
+ end
43
+ end
44
+
45
+ # @!macro common_attrs
46
+ # @option attrs [String] :message_id ID that uniquely
47
+ # identifies a message across the API. (optional)
48
+ # @option attrs [Time] :timestamp When the event occurred (optional)
49
+ # @option attrs [String] :distinct_id The ID for this user in your database
50
+
51
+ # Captures an event
52
+ #
53
+ # @param [Hash] attrs
54
+ #
55
+ # @option attrs [String] :event Event name
56
+ # @option attrs [Hash] :properties Event properties (optional)
57
+ # @macro common_attrs
58
+ def capture(attrs)
59
+ symbolize_keys! attrs
60
+ enqueue(FieldParser.parse_for_capture(attrs))
61
+ end
62
+
63
+ # Identifies a user
64
+ #
65
+ # @param [Hash] attrs
66
+ #
67
+ # @option attrs [Hash] :properties User properties (optional)
68
+ # @macro common_attrs
69
+ def identify(attrs)
70
+ symbolize_keys! attrs
71
+ enqueue(FieldParser.parse_for_identify(attrs))
72
+ end
73
+
74
+ # Aliases a user from one id to another
75
+ #
76
+ # @param [Hash] attrs
77
+ #
78
+ # @option attrs [String] :alias The alias to give the distinct id
79
+ # @macro common_attrs
80
+ def alias(attrs)
81
+ symbolize_keys! attrs
82
+ enqueue(FieldParser.parse_for_alias(attrs))
83
+ end
84
+
85
+ # @return [Fixnum] number of messages in the queue
86
+ def queued_messages
87
+ @queue.length
88
+ end
89
+
90
+ private
91
+
92
+ # private: Enqueues the action.
93
+ #
94
+ # returns Boolean of whether the item was added to the queue.
95
+ def enqueue(action)
96
+ # add our request id for tracing purposes
97
+ action[:messageId] ||= uid
98
+
99
+ if @queue.length < @max_queue_size
100
+ @queue << action
101
+ ensure_worker_running
102
+
103
+ true
104
+ else
105
+ logger.warn(
106
+ 'Queue is full, dropping events. The :max_queue_size ' \
107
+ 'configuration parameter can be increased to prevent this from ' \
108
+ 'happening.'
109
+ )
110
+ false
111
+ end
112
+ end
113
+
114
+ # private: Checks that the api_key is properly initialized
115
+ def check_api_key!
116
+ raise ArgumentError, 'API key must be initialized' if @api_key.nil?
117
+ end
118
+
119
+ def ensure_worker_running
120
+ return if worker_running?
121
+ @worker_mutex.synchronize do
122
+ return if worker_running?
123
+ @worker_thread = Thread.new do
124
+ @worker.run
125
+ end
126
+ end
127
+ end
128
+
129
+ def worker_running?
130
+ @worker_thread && @worker_thread.alive?
131
+ end
132
+ end
133
+ end
134
+
@@ -0,0 +1,34 @@
1
+ class PostHog
2
+ module Defaults
3
+ module Request
4
+ HOST = 't.posthog.com'
5
+ PORT = 443
6
+ PATH = '/batch/'
7
+ SSL = true
8
+ HEADERS = { 'Accept' => 'application/json',
9
+ 'Content-Type' => 'application/json',
10
+ 'User-Agent' => "posthog-ruby/#{PostHog::VERSION}" }
11
+ RETRIES = 10
12
+ end
13
+
14
+ module Queue
15
+ MAX_SIZE = 10000
16
+ end
17
+
18
+ module Message
19
+ MAX_BYTES = 32768 # 32Kb
20
+ end
21
+
22
+ module MessageBatch
23
+ MAX_BYTES = 512_000 # 500Kb
24
+ MAX_SIZE = 100
25
+ end
26
+
27
+ module BackoffPolicy
28
+ MIN_TIMEOUT_MS = 100
29
+ MAX_TIMEOUT_MS = 10000
30
+ MULTIPLIER = 1.5
31
+ RANDOMIZATION_FACTOR = 0.5
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,112 @@
1
+ class PostHog
2
+ class FieldParser
3
+ class << self
4
+ include PostHog::Utils
5
+
6
+ # In addition to the common fields, capture accepts:
7
+ #
8
+ # - "event"
9
+ # - "properties"
10
+ def parse_for_capture(fields)
11
+ common = parse_common_fields(fields)
12
+
13
+ event = fields[:event]
14
+ properties = fields[:properties] || {}
15
+
16
+ check_presence!(event, 'event')
17
+ check_is_hash!(properties, 'properties')
18
+
19
+ isoify_dates! properties
20
+
21
+ common.merge({
22
+ :type => 'capture',
23
+ :event => event.to_s,
24
+ :properties => properties
25
+ })
26
+ end
27
+
28
+ # In addition to the common fields, identify accepts:
29
+ #
30
+ # - "properties"
31
+ def parse_for_identify(fields)
32
+ common = parse_common_fields(fields)
33
+
34
+ properties = fields[:properties] || {}
35
+ check_is_hash!(properties, 'properties')
36
+
37
+ isoify_dates! properties
38
+
39
+ common.merge({
40
+ :type => 'identify',
41
+ :event => '$identify',
42
+ :'$set' => properties
43
+ })
44
+ end
45
+
46
+ # In addition to the common fields, alias accepts:
47
+ #
48
+ # - "alias"
49
+ def parse_for_alias(fields)
50
+ common = parse_common_fields(fields)
51
+
52
+ distinct_id = common[:distinct_id] # must move to properties...
53
+
54
+ alias_field = fields[:alias]
55
+ check_presence! alias_field, 'alias'
56
+
57
+ common.merge({
58
+ :type => 'alias',
59
+ :event => '$create_alias',
60
+ :distinct_id => nil,
61
+ :properties => {
62
+ :distinct_id => distinct_id,
63
+ :alias => alias_field,
64
+ }
65
+ })
66
+ end
67
+
68
+ private
69
+
70
+ # Common fields are:
71
+ #
72
+ # - "timestamp"
73
+ # - "distinct_id"
74
+ # - "message_id"
75
+ def parse_common_fields(fields)
76
+ timestamp = fields[:timestamp] || Time.new
77
+ distinct_id = fields[:distinct_id]
78
+ message_id = fields[:message_id].to_s if fields[:message_id]
79
+
80
+ check_timestamp! timestamp
81
+ check_presence! distinct_id, 'distinct_id'
82
+
83
+ parsed = {
84
+ :timestamp => datetime_in_iso8601(timestamp),
85
+ :library => 'posthog-ruby',
86
+ :library_version => PostHog::VERSION.to_s,
87
+ :messageId => message_id,
88
+ :distinct_id => distinct_id
89
+ }
90
+ parsed
91
+ end
92
+
93
+ def check_timestamp!(timestamp)
94
+ raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
95
+ end
96
+
97
+ # private: Ensures that a string is non-empty
98
+ #
99
+ # obj - String|Number that must be non-blank
100
+ # name - Name of the validated value
101
+ def check_presence!(obj, name)
102
+ if obj.nil? || (obj.is_a?(String) && obj.empty?)
103
+ raise ArgumentError, "#{name} must be given"
104
+ end
105
+ end
106
+
107
+ def check_is_hash!(obj, name)
108
+ raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,59 @@
1
+ require 'logger'
2
+
3
+ class PostHog
4
+ # Wraps an existing logger and adds a prefix to all messages
5
+ class PrefixedLogger
6
+ def initialize(logger, prefix)
7
+ @logger = logger
8
+ @prefix = prefix
9
+ end
10
+
11
+ def debug(msg)
12
+ @logger.debug("#{@prefix} #{msg}")
13
+ end
14
+
15
+ def info(msg)
16
+ @logger.info("#{@prefix} #{msg}")
17
+ end
18
+
19
+ def warn(msg)
20
+ @logger.warn("#{@prefix} #{msg}")
21
+ end
22
+
23
+ def error(msg)
24
+ @logger.error("#{@prefix} #{msg}")
25
+ end
26
+ end
27
+
28
+ module Logging
29
+ class << self
30
+ def logger
31
+ return @logger if @logger
32
+
33
+ base_logger = if defined?(Rails)
34
+ Rails.logger
35
+ else
36
+ logger = Logger.new STDOUT
37
+ logger.progname = 'PostHog'
38
+ logger
39
+ end
40
+ @logger = PrefixedLogger.new(base_logger, '[posthog-ruby]')
41
+ end
42
+
43
+ attr_writer :logger
44
+ end
45
+
46
+ def self.included(base)
47
+ class << base
48
+ def logger
49
+ Logging.logger
50
+ end
51
+ end
52
+ end
53
+
54
+ def logger
55
+ Logging.logger
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,70 @@
1
+ require 'forwardable'
2
+ require 'posthog/logging'
3
+
4
+ class PostHog
5
+ # A batch of `Message`s to be sent to the API
6
+ class MessageBatch
7
+ class JSONGenerationError < StandardError; end
8
+
9
+ extend Forwardable
10
+ include PostHog::Logging
11
+ include PostHog::Defaults::MessageBatch
12
+
13
+ def initialize(max_message_count)
14
+ @messages = []
15
+ @max_message_count = max_message_count
16
+ @json_size = 0
17
+ end
18
+
19
+ def <<(message)
20
+ begin
21
+ message_json = message.to_json
22
+ rescue StandardError => e
23
+ raise JSONGenerationError, "Serialization error: #{e}"
24
+ end
25
+
26
+ message_json_size = message_json.bytesize
27
+ if message_too_big?(message_json_size)
28
+ logger.error('a message exceeded the maximum allowed size')
29
+ else
30
+ @messages << message
31
+ @json_size += message_json_size + 1 # One byte for the comma
32
+ end
33
+ end
34
+
35
+ def full?
36
+ item_count_exhausted? || size_exhausted?
37
+ end
38
+
39
+ def clear
40
+ @messages.clear
41
+ @json_size = 0
42
+ end
43
+
44
+ def_delegators :@messages, :to_json
45
+ def_delegators :@messages, :empty?
46
+ def_delegators :@messages, :length
47
+
48
+ private
49
+
50
+ def item_count_exhausted?
51
+ @messages.length >= @max_message_count
52
+ end
53
+
54
+ def message_too_big?(message_json_size)
55
+ message_json_size > Defaults::Message::MAX_BYTES
56
+ end
57
+
58
+ # We consider the max size here as just enough to leave room for one more
59
+ # message of the largest size possible. This is a shortcut that allows us
60
+ # to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff
61
+ # here is that we might fit in less messages than possible into a batch.
62
+ #
63
+ # The alternative is to use our own `Queue` implementation that allows
64
+ # peeking, and to consider the next message size when calculating whether
65
+ # the message can be accomodated in this batch.
66
+ def size_exhausted?
67
+ @json_size >= (MAX_BYTES - Defaults::Message::MAX_BYTES)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,14 @@
1
+ class PostHog
2
+ class Response
3
+ attr_reader :status, :error
4
+
5
+ # public: Simple class to wrap responses from the API
6
+ #
7
+ #
8
+ def initialize(status = 200, error = nil)
9
+ @status = status
10
+ @error = error
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,143 @@
1
+ require 'posthog/defaults'
2
+ require 'posthog/utils'
3
+ require 'posthog/response'
4
+ require 'posthog/logging'
5
+ require 'posthog/backoff_policy'
6
+ require 'net/http'
7
+ require 'net/https'
8
+ require 'json'
9
+
10
+ class PostHog
11
+ class Transport
12
+ include PostHog::Defaults::Request
13
+ include PostHog::Utils
14
+ include PostHog::Logging
15
+
16
+ def initialize(options = {})
17
+ if options[:api_host]
18
+ uri = URI.parse(options[:api_host])
19
+ options[:host] = uri.host
20
+ options[:ssl] = uri.scheme == 'https'
21
+ options[:port] = uri.port
22
+ end
23
+ options[:host] ||= HOST
24
+ options[:port] ||= PORT
25
+ options[:ssl] ||= SSL
26
+ @headers = options[:headers] || HEADERS
27
+ @path = options[:path] || PATH
28
+ @retries = options[:retries] || RETRIES
29
+ @backoff_policy =
30
+ options[:backoff_policy] || PostHog::BackoffPolicy.new
31
+
32
+ http = Net::HTTP.new(options[:host], options[:port])
33
+ http.use_ssl = options[:ssl]
34
+ http.read_timeout = 8
35
+ http.open_timeout = 4
36
+
37
+ @http = http
38
+ end
39
+
40
+ # Sends a batch of messages to the API
41
+ #
42
+ # @return [Response] API response
43
+ def send(api_key, batch)
44
+ logger.debug("Sending request for #{batch.length} items")
45
+
46
+ last_response, exception = retry_with_backoff(@retries) do
47
+ status_code, body = send_request(api_key, batch)
48
+ error = JSON.parse(body)['error']
49
+ should_retry = should_retry_request?(status_code, body)
50
+ logger.debug("Response status code: #{status_code}")
51
+ logger.debug("Response error: #{error}") if error
52
+
53
+ [Response.new(status_code, error), should_retry]
54
+ end
55
+
56
+ if exception
57
+ logger.error(exception.message)
58
+ exception.backtrace.each { |line| logger.error(line) }
59
+ Response.new(-1, exception.to_s)
60
+ else
61
+ last_response
62
+ end
63
+ end
64
+
65
+ # Closes a persistent connection if it exists
66
+ def shutdown
67
+ @http.finish if @http.started?
68
+ end
69
+
70
+ private
71
+
72
+ def should_retry_request?(status_code, body)
73
+ if status_code >= 500
74
+ true # Server error
75
+ elsif status_code == 429
76
+ true # Rate limited
77
+ elsif status_code >= 400
78
+ logger.error(body)
79
+ false # Client error. Do not retry, but log
80
+ else
81
+ false
82
+ end
83
+ end
84
+
85
+ # Takes a block that returns [result, should_retry].
86
+ #
87
+ # Retries upto `retries_remaining` times, if `should_retry` is false or
88
+ # an exception is raised. `@backoff_policy` is used to determine the
89
+ # duration to sleep between attempts
90
+ #
91
+ # Returns [last_result, raised_exception]
92
+ def retry_with_backoff(retries_remaining, &block)
93
+ result, caught_exception = nil
94
+ should_retry = false
95
+
96
+ begin
97
+ result, should_retry = yield
98
+ return [result, nil] unless should_retry
99
+ rescue StandardError => e
100
+ should_retry = true
101
+ caught_exception = e
102
+ end
103
+
104
+ if should_retry && (retries_remaining > 1)
105
+ logger.debug("Retrying request, #{retries_remaining} retries left")
106
+ sleep(@backoff_policy.next_interval.to_f / 1000)
107
+ retry_with_backoff(retries_remaining - 1, &block)
108
+ else
109
+ [result, caught_exception]
110
+ end
111
+ end
112
+
113
+ # Sends a request for the batch, returns [status_code, body]
114
+ def send_request(api_key, batch)
115
+ payload = JSON.generate(
116
+ api_key: api_key,
117
+ batch: batch
118
+ )
119
+
120
+ request = Net::HTTP::Post.new(@path, @headers)
121
+
122
+ if self.class.stub
123
+ logger.debug "stubbed request to #{@path}: " \
124
+ "api key = #{api_key}, batch = #{JSON.generate(batch)}"
125
+
126
+ [200, '{}']
127
+ else
128
+ @http.start unless @http.started? # Maintain a persistent connection
129
+ response = @http.request(request, payload)
130
+ [response.code.to_i, response.body]
131
+ end
132
+ end
133
+
134
+ class << self
135
+ attr_writer :stub
136
+
137
+ def stub
138
+ @stub || ENV['STUB']
139
+ end
140
+ end
141
+ end
142
+ end
143
+
@@ -0,0 +1,90 @@
1
+ require 'securerandom'
2
+
3
+ class PostHog
4
+ module Utils
5
+ extend self
6
+
7
+ # public: Return a new hash with keys converted from strings to symbols
8
+ #
9
+ def symbolize_keys(hash)
10
+ hash.each_with_object({}) do |(k, v), memo|
11
+ memo[k.to_sym] = v
12
+ end
13
+ end
14
+
15
+ # public: Convert hash keys from strings to symbols in place
16
+ #
17
+ def symbolize_keys!(hash)
18
+ hash.replace symbolize_keys hash
19
+ end
20
+
21
+ # public: Return a new hash with keys as strings
22
+ #
23
+ def stringify_keys(hash)
24
+ hash.each_with_object({}) do |(k, v), memo|
25
+ memo[k.to_s] = v
26
+ end
27
+ end
28
+
29
+ # public: Returns a new hash with all the date values in the into iso8601
30
+ # strings
31
+ #
32
+ def isoify_dates(hash)
33
+ hash.each_with_object({}) do |(k, v), memo|
34
+ memo[k] = datetime_in_iso8601(v)
35
+ end
36
+ end
37
+
38
+ # public: Converts all the date values in the into iso8601 strings in place
39
+ #
40
+ def isoify_dates!(hash)
41
+ hash.replace isoify_dates hash
42
+ end
43
+
44
+ # public: Returns a uid string
45
+ #
46
+ def uid
47
+ arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
48
+ arr[2] = (arr[2] & 0x0fff) | 0x4000
49
+ arr[3] = (arr[3] & 0x3fff) | 0x8000
50
+ '%08x-%04x-%04x-%04x-%04x%08x' % arr
51
+ end
52
+
53
+ def datetime_in_iso8601(datetime)
54
+ case datetime
55
+ when Time
56
+ time_in_iso8601 datetime
57
+ when DateTime
58
+ time_in_iso8601 datetime.to_time
59
+ when Date
60
+ date_in_iso8601 datetime
61
+ else
62
+ datetime
63
+ end
64
+ end
65
+
66
+ def time_in_iso8601(time, fraction_digits = 3)
67
+ fraction = if fraction_digits > 0
68
+ ('.%06i' % time.usec)[0, fraction_digits + 1]
69
+ end
70
+
71
+ "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}"
72
+ end
73
+
74
+ def date_in_iso8601(date)
75
+ date.strftime('%F')
76
+ end
77
+
78
+ def formatted_offset(time, colon = true, alternate_utc_string = nil)
79
+ time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
80
+ end
81
+
82
+ def seconds_to_utc_offset(seconds, colon = true)
83
+ (colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON) % [(seconds < 0 ? '-' : '+'), (seconds.abs / 3600), ((seconds.abs % 3600) / 60)]
84
+ end
85
+
86
+ UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
87
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
88
+ end
89
+ end
90
+
@@ -0,0 +1,3 @@
1
+ class PostHog
2
+ VERSION = '1.0.0.pre'
3
+ end
@@ -0,0 +1,68 @@
1
+ require 'posthog/defaults'
2
+ require 'posthog/message_batch'
3
+ require 'posthog/transport'
4
+ require 'posthog/utils'
5
+
6
+ class PostHog
7
+ class Worker
8
+ include PostHog::Utils
9
+ include PostHog::Defaults
10
+ include PostHog::Logging
11
+
12
+ # public: Creates a new worker
13
+ #
14
+ # The worker continuously takes messages off the queue
15
+ # and makes requests to the posthog.com api
16
+ #
17
+ # queue - Queue synchronized between client and worker
18
+ # api_key - String of the project's API key
19
+ # options - Hash of worker options
20
+ # batch_size - Fixnum of how many items to send in a batch
21
+ # on_error - Proc of what to do on an error
22
+ #
23
+ def initialize(queue, api_key, options = {})
24
+ symbolize_keys! options
25
+ @queue = queue
26
+ @api_key = api_key
27
+ @on_error = options[:on_error] || proc { |status, error| }
28
+ batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
29
+ @batch = MessageBatch.new(batch_size)
30
+ @lock = Mutex.new
31
+ @transport = Transport.new api_host: options[:api_host]
32
+ end
33
+
34
+ # public: Continuously runs the loop to check for new events
35
+ #
36
+ def run
37
+ until Thread.current[:should_exit]
38
+ return if @queue.empty?
39
+
40
+ @lock.synchronize do
41
+ consume_message_from_queue! until @batch.full? || @queue.empty?
42
+ end
43
+
44
+ res = @transport.send @api_key, @batch
45
+ @on_error.call(res.status, res.error) unless res.status == 200
46
+
47
+ @lock.synchronize { @batch.clear }
48
+ end
49
+ ensure
50
+ @transport.shutdown
51
+ end
52
+
53
+ # public: Check whether we have outstanding requests.
54
+ #
55
+ def is_requesting?
56
+ @lock.synchronize { !@batch.empty? }
57
+ end
58
+
59
+ private
60
+
61
+ def consume_message_from_queue!
62
+ @batch << @queue.pop
63
+ rescue MessageBatch::JSONGenerationError => e
64
+ @on_error.call(-1, e.to_s)
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1 @@
1
+ require 'posthog'
data/lib/posthog.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'posthog/version'
2
+ require 'posthog/defaults'
3
+ require 'posthog/utils'
4
+ require 'posthog/field_parser'
5
+ require 'posthog/client'
6
+ require 'posthog/worker'
7
+ require 'posthog/transport'
8
+ require 'posthog/response'
9
+ require 'posthog/logging'
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: posthog-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - ''
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: commander
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tzinfo
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 4.1.11
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.1.11
83
+ - !ruby/object:Gem::Dependency
84
+ name: oj
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.6.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.6.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.51.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.51.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: codecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.1.4
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.1.4
125
+ description: The PostHog ruby library
126
+ email: hey@posthog.com
127
+ executables:
128
+ - posthog
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - bin/posthog
133
+ - lib/posthog-ruby.rb
134
+ - lib/posthog.rb
135
+ - lib/posthog/backoff_policy.rb
136
+ - lib/posthog/client.rb
137
+ - lib/posthog/defaults.rb
138
+ - lib/posthog/field_parser.rb
139
+ - lib/posthog/logging.rb
140
+ - lib/posthog/message_batch.rb
141
+ - lib/posthog/response.rb
142
+ - lib/posthog/transport.rb
143
+ - lib/posthog/utils.rb
144
+ - lib/posthog/version.rb
145
+ - lib/posthog/worker.rb
146
+ homepage: https://github.com/PostHog/posthog-ruby
147
+ licenses:
148
+ - MIT
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '2.0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">"
162
+ - !ruby/object:Gem::Version
163
+ version: 1.3.1
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.5.2.3
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: PostHog library
170
+ test_files: []