castle-rb 3.6.2
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 +7 -0
- data/README.md +157 -0
- data/lib/castle-rb.rb +3 -0
- data/lib/castle.rb +62 -0
- data/lib/castle/api.rb +40 -0
- data/lib/castle/api/request.rb +37 -0
- data/lib/castle/api/request/build.rb +27 -0
- data/lib/castle/api/response.rb +40 -0
- data/lib/castle/client.rb +106 -0
- data/lib/castle/command.rb +5 -0
- data/lib/castle/commands/authenticate.rb +23 -0
- data/lib/castle/commands/identify.rb +23 -0
- data/lib/castle/commands/impersonate.rb +26 -0
- data/lib/castle/commands/review.rb +14 -0
- data/lib/castle/commands/track.rb +23 -0
- data/lib/castle/configuration.rb +80 -0
- data/lib/castle/context/default.rb +40 -0
- data/lib/castle/context/merger.rb +14 -0
- data/lib/castle/context/sanitizer.rb +23 -0
- data/lib/castle/errors.rb +41 -0
- data/lib/castle/extractors/client_id.rb +17 -0
- data/lib/castle/extractors/headers.rb +51 -0
- data/lib/castle/extractors/ip.rb +18 -0
- data/lib/castle/failover_auth_response.rb +21 -0
- data/lib/castle/header_formatter.rb +9 -0
- data/lib/castle/review.rb +11 -0
- data/lib/castle/secure_mode.rb +11 -0
- data/lib/castle/support/hanami.rb +19 -0
- data/lib/castle/support/padrino.rb +19 -0
- data/lib/castle/support/rails.rb +13 -0
- data/lib/castle/support/sinatra.rb +19 -0
- data/lib/castle/utils.rb +55 -0
- data/lib/castle/utils/cloner.rb +11 -0
- data/lib/castle/utils/merger.rb +23 -0
- data/lib/castle/utils/timestamp.rb +12 -0
- data/lib/castle/validators/not_supported.rb +16 -0
- data/lib/castle/validators/present.rb +16 -0
- data/lib/castle/version.rb +5 -0
- data/spec/lib/castle/api/request/build_spec.rb +44 -0
- data/spec/lib/castle/api/request_spec.rb +59 -0
- data/spec/lib/castle/api/response_spec.rb +58 -0
- data/spec/lib/castle/api_spec.rb +37 -0
- data/spec/lib/castle/client_spec.rb +358 -0
- data/spec/lib/castle/command_spec.rb +9 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +108 -0
- data/spec/lib/castle/commands/identify_spec.rb +87 -0
- data/spec/lib/castle/commands/impersonate_spec.rb +106 -0
- data/spec/lib/castle/commands/review_spec.rb +24 -0
- data/spec/lib/castle/commands/track_spec.rb +113 -0
- data/spec/lib/castle/configuration_spec.rb +130 -0
- data/spec/lib/castle/context/default_spec.rb +41 -0
- data/spec/lib/castle/context/merger_spec.rb +23 -0
- data/spec/lib/castle/context/sanitizer_spec.rb +27 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +62 -0
- data/spec/lib/castle/extractors/headers_spec.rb +89 -0
- data/spec/lib/castle/extractors/ip_spec.rb +27 -0
- data/spec/lib/castle/header_formatter_spec.rb +25 -0
- data/spec/lib/castle/review_spec.rb +19 -0
- data/spec/lib/castle/secure_mode_spec.rb +9 -0
- data/spec/lib/castle/utils/cloner_spec.rb +18 -0
- data/spec/lib/castle/utils/merger_spec.rb +13 -0
- data/spec/lib/castle/utils/timestamp_spec.rb +17 -0
- data/spec/lib/castle/utils_spec.rb +156 -0
- data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
- data/spec/lib/castle/validators/present_spec.rb +33 -0
- data/spec/lib/castle/version_spec.rb +5 -0
- data/spec/lib/castle_spec.rb +66 -0
- data/spec/spec_helper.rb +25 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 01fa92f200806e9649d71afa71dff0e8ff37db950a74e122134fdbda5413ef8a
|
4
|
+
data.tar.gz: f3a11d66711788d68c228ff63b647326e5446fff0ddb1be4e33e4b400d4587a8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5329a71bd213ee88665b68ab4a79f6eca7987355d839d9e26b4d132ea3d4b7986714d23c3b6f97e7743ce774d969438365ea004fdcdd99eb7643f5c07c43d7b9
|
7
|
+
data.tar.gz: 89d8241793e59288651ac45b8d799c545a9b60aa25e6ae8d0092cda6183e9ecdf3b9d5b8ee68e0184b8021dc8633f9f949c987c6f0cd3abc1db6f7f2b47bda99
|
data/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# Ruby SDK for Castle
|
2
|
+
|
3
|
+
[](https://travis-ci.org/castle/castle-ruby)
|
4
|
+
[](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
|
5
|
+
[](https://badge.fury.io/rb/castle-rb)
|
6
|
+
|
7
|
+
**[Castle](https://castle.io) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..**
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add the `castle-rb` gem to your `Gemfile`
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'castle-rb'
|
15
|
+
```
|
16
|
+
|
17
|
+
## Configuration
|
18
|
+
|
19
|
+
### Framework configuration
|
20
|
+
|
21
|
+
Load and configure the library with your Castle API secret in an initializer or similar.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
Castle.api_secret = 'YOUR_API_SECRET'
|
25
|
+
```
|
26
|
+
|
27
|
+
A Castle client instance will be made available as `castle` in your
|
28
|
+
|
29
|
+
* Rails controllers when you add `require 'castle/support/rails'`
|
30
|
+
|
31
|
+
* Padrino controllers when you add `require 'castle/support/padrino'`
|
32
|
+
|
33
|
+
* Sinatra app when you add `require 'castle/support/sinatra'` (and additionally explicitly add `register Sinatra::Castle` to your `Sinatra::Base` class if you have a modular application)
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
require 'castle/support/sinatra'
|
37
|
+
|
38
|
+
class ApplicationController < Sinatra::Base
|
39
|
+
register Sinatra::Castle
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
* Hanami when you add `require 'castle/support/hanami'` and include `Castle::Hanami` to your Hanami application
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'castle/support/hanami'
|
47
|
+
|
48
|
+
module Web
|
49
|
+
class Application < Hanami::Application
|
50
|
+
include Castle::Hanami
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### Client configuration
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Castle.configure do |config|
|
59
|
+
# Same as setting it through Castle.api_secret
|
60
|
+
config.api_secret = 'secret'
|
61
|
+
|
62
|
+
# For authenticate method you can set failover strategies: allow(default), deny, challenge, throw
|
63
|
+
config.failover_strategy = :deny
|
64
|
+
|
65
|
+
# Castle::RequestError is raised when timing out in milliseconds (default: 500 milliseconds)
|
66
|
+
config.request_timeout = 2000
|
67
|
+
|
68
|
+
# Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
|
69
|
+
# Whitelisted headers
|
70
|
+
# By default, the SDK sends all HTTP headers, except for Cookie and Authorization.
|
71
|
+
# If you decide to use a whitelist, the SDK will:
|
72
|
+
# - always send the User-Agent header
|
73
|
+
# - send scrubbed values of non-whitelisted headers
|
74
|
+
# - send proper values of whitelisted headers.
|
75
|
+
# @example
|
76
|
+
# config.whitelisted = ['X_HEADER']
|
77
|
+
# # will send { 'User-Agent' => 'Chrome', 'X_HEADER' => 'proper value', 'Any-Other-Header' => true }
|
78
|
+
#
|
79
|
+
# We highly suggest using blacklist instead of whitelist, so that Castle can use as many data points
|
80
|
+
# as possible to secure your users. If you want to use the whitelist, this is the minimal
|
81
|
+
# amount of headers we recommend:
|
82
|
+
config.whitelisted = Castle::Configuration::DEFAULT_WHITELIST
|
83
|
+
|
84
|
+
# Blacklisted headers take precedence over whitelisted elements
|
85
|
+
# We always blacklist Cookie and Authentication headers. If you use any other headers that
|
86
|
+
# might contain sensitive information, you should blacklist them.
|
87
|
+
config.blacklisted = ['HTTP-X-header']
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
The client will automatically configure the context for each request.
|
92
|
+
|
93
|
+
## Tracking
|
94
|
+
|
95
|
+
Here is a simple example of a track event.
|
96
|
+
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
begin
|
100
|
+
castle.track(
|
101
|
+
event: '$login.succeeded',
|
102
|
+
user_id: user.id
|
103
|
+
)
|
104
|
+
rescue Castle::Error => e
|
105
|
+
puts e.message
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
## Signature
|
110
|
+
|
111
|
+
`Castle::SecureMode.signature(user_id)` will create a signed user_id.
|
112
|
+
|
113
|
+
## Async tracking
|
114
|
+
|
115
|
+
By default Castle sends requests synchronously. To eg. use Sidekiq to send requests in a background worker you can pass data to the worker:
|
116
|
+
|
117
|
+
#### castle_tracking_worker.rb
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class CastleTrackingWorker
|
121
|
+
include Sidekiq::Worker
|
122
|
+
|
123
|
+
def perform(context, track_options = {})
|
124
|
+
client = ::Castle::Client.new(context)
|
125
|
+
client.track(track_options)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
#### tracking_controller.rb
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
request_context = ::Castle::Client.to_context(request)
|
134
|
+
track_options = ::Castle::Client.to_options({
|
135
|
+
event: '$login.succeeded',
|
136
|
+
user_id: user.id,
|
137
|
+
properties: {
|
138
|
+
key: 'value'
|
139
|
+
},
|
140
|
+
user_traits: {
|
141
|
+
key: 'value'
|
142
|
+
}
|
143
|
+
})
|
144
|
+
CastleTrackingWorker.perform_async(request_context, track_options)
|
145
|
+
```
|
146
|
+
|
147
|
+
## Impersonation mode
|
148
|
+
|
149
|
+
https://castle.io/docs/impersonation
|
150
|
+
|
151
|
+
## Exceptions
|
152
|
+
|
153
|
+
`Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
|
154
|
+
|
155
|
+
## Documentation
|
156
|
+
|
157
|
+
[Official Castle docs](https://castle.io/docs)
|
data/lib/castle-rb.rb
ADDED
data/lib/castle.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
%w[
|
4
|
+
openssl
|
5
|
+
net/http
|
6
|
+
json
|
7
|
+
time
|
8
|
+
].each(&method(:require))
|
9
|
+
|
10
|
+
%w[
|
11
|
+
castle/version
|
12
|
+
castle/errors
|
13
|
+
castle/command
|
14
|
+
castle/utils
|
15
|
+
castle/utils/merger
|
16
|
+
castle/utils/cloner
|
17
|
+
castle/utils/timestamp
|
18
|
+
castle/validators/present
|
19
|
+
castle/validators/not_supported
|
20
|
+
castle/context/merger
|
21
|
+
castle/context/sanitizer
|
22
|
+
castle/context/default
|
23
|
+
castle/commands/identify
|
24
|
+
castle/commands/authenticate
|
25
|
+
castle/commands/track
|
26
|
+
castle/commands/review
|
27
|
+
castle/commands/impersonate
|
28
|
+
castle/configuration
|
29
|
+
castle/failover_auth_response
|
30
|
+
castle/client
|
31
|
+
castle/header_formatter
|
32
|
+
castle/secure_mode
|
33
|
+
castle/extractors/client_id
|
34
|
+
castle/extractors/headers
|
35
|
+
castle/extractors/ip
|
36
|
+
castle/api/response
|
37
|
+
castle/api/request
|
38
|
+
castle/api/request/build
|
39
|
+
castle/review
|
40
|
+
castle/api
|
41
|
+
].each(&method(:require))
|
42
|
+
|
43
|
+
# main sdk module
|
44
|
+
module Castle
|
45
|
+
class << self
|
46
|
+
def configure(config_hash = nil)
|
47
|
+
(config_hash || {}).each do |config_name, config_value|
|
48
|
+
config.send("#{config_name}=", config_value)
|
49
|
+
end
|
50
|
+
|
51
|
+
yield(config) if block_given?
|
52
|
+
end
|
53
|
+
|
54
|
+
def config
|
55
|
+
@configuration ||= Castle::Configuration.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def api_secret=(api_secret)
|
59
|
+
config.api_secret = api_secret
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/castle/api.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
module API
|
6
|
+
# Errors we handle internally
|
7
|
+
HANDLED_ERRORS = [
|
8
|
+
Timeout::Error,
|
9
|
+
Errno::EINVAL,
|
10
|
+
Errno::ECONNRESET,
|
11
|
+
EOFError,
|
12
|
+
Net::HTTPBadResponse,
|
13
|
+
Net::HTTPHeaderSyntaxError,
|
14
|
+
Net::ProtocolError
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
private_constant :HANDLED_ERRORS
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def request(command, headers = {})
|
21
|
+
raise Castle::ConfigurationError, 'configuration is not valid' unless Castle.config.valid?
|
22
|
+
|
23
|
+
begin
|
24
|
+
Castle::API::Response.call(
|
25
|
+
Castle::API::Request.call(
|
26
|
+
command,
|
27
|
+
Castle.config.api_secret,
|
28
|
+
headers
|
29
|
+
)
|
30
|
+
)
|
31
|
+
rescue *HANDLED_ERRORS => e
|
32
|
+
# @note We need to initialize the error, as the original error is a cause for this
|
33
|
+
# custom exception. If we would do it the default Ruby way, the original error
|
34
|
+
# would get converted into a string
|
35
|
+
raise Castle::RequestError.new(e) # rubocop:disable Style/RaiseArgs
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
module API
|
6
|
+
module Request
|
7
|
+
# Default headers that we add to passed ones
|
8
|
+
DEFAULT_HEADERS = {
|
9
|
+
'Content-Type' => 'application/json'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
private_constant :DEFAULT_HEADERS
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def call(command, api_secret, headers)
|
16
|
+
http.request(
|
17
|
+
Castle::API::Request::Build.call(
|
18
|
+
command,
|
19
|
+
headers.merge(DEFAULT_HEADERS),
|
20
|
+
api_secret
|
21
|
+
)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def http
|
26
|
+
http = Net::HTTP.new(Castle.config.host, Castle.config.port)
|
27
|
+
http.read_timeout = Castle.config.request_timeout / 1000.0
|
28
|
+
if Castle.config.port == 443
|
29
|
+
http.use_ssl = true
|
30
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
31
|
+
end
|
32
|
+
http
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module API
|
5
|
+
# generate api request
|
6
|
+
module Request
|
7
|
+
module Build
|
8
|
+
class << self
|
9
|
+
def call(command, headers, api_secret)
|
10
|
+
request = Net::HTTP.const_get(
|
11
|
+
command.method.to_s.capitalize
|
12
|
+
).new("/#{Castle.config.url_prefix}/#{command.path}", headers)
|
13
|
+
|
14
|
+
unless command.method == :get
|
15
|
+
request.body = ::Castle::Utils.replace_invalid_characters(
|
16
|
+
command.data
|
17
|
+
).to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
request.basic_auth('', api_secret)
|
21
|
+
request
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module API
|
5
|
+
# parses api response
|
6
|
+
module Response
|
7
|
+
RESPONSE_ERRORS = {
|
8
|
+
400 => Castle::BadRequestError,
|
9
|
+
401 => Castle::UnauthorizedError,
|
10
|
+
403 => Castle::ForbiddenError,
|
11
|
+
404 => Castle::NotFoundError,
|
12
|
+
419 => Castle::UserUnauthorizedError,
|
13
|
+
422 => Castle::InvalidParametersError
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def call(response)
|
18
|
+
verify!(response)
|
19
|
+
|
20
|
+
return {} if response.body.nil? || response.body.empty?
|
21
|
+
|
22
|
+
begin
|
23
|
+
JSON.parse(response.body, symbolize_names: true)
|
24
|
+
rescue JSON::ParserError
|
25
|
+
raise Castle::ApiError, 'Invalid response from Castle API'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify!(response)
|
30
|
+
return if response.code.to_i.between?(200, 299)
|
31
|
+
|
32
|
+
raise Castle::InternalServerError if response.code.to_i.between?(500, 599)
|
33
|
+
|
34
|
+
error = RESPONSE_ERRORS.fetch(response.code.to_i, Castle::ApiError)
|
35
|
+
raise error, response[:message]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
class Client
|
5
|
+
class << self
|
6
|
+
def from_request(request, options = {})
|
7
|
+
new(
|
8
|
+
to_context(request, options),
|
9
|
+
to_options(options)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_context(request, options = {})
|
14
|
+
default_context = Castle::Context::Default.new(request, options[:cookies]).call
|
15
|
+
Castle::Context::Merger.call(default_context, options[:context])
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_options(options = {})
|
19
|
+
options[:timestamp] ||= Castle::Utils::Timestamp.call
|
20
|
+
warn '[DEPRECATION] use user_traits instead of traits key' if options.key?(:traits)
|
21
|
+
options
|
22
|
+
end
|
23
|
+
|
24
|
+
def failover_response_or_raise(failover_response, error)
|
25
|
+
return failover_response.generate unless Castle.config.failover_strategy == :throw
|
26
|
+
raise error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :context
|
31
|
+
|
32
|
+
def initialize(context, options = {})
|
33
|
+
@do_not_track = options.fetch(:do_not_track, false)
|
34
|
+
@timestamp = options[:timestamp]
|
35
|
+
@context = context
|
36
|
+
end
|
37
|
+
|
38
|
+
def authenticate(options = {})
|
39
|
+
options = Castle::Utils.deep_symbolize_keys(options || {})
|
40
|
+
|
41
|
+
if tracked?
|
42
|
+
add_timestamp_if_necessary(options)
|
43
|
+
command = Castle::Commands::Authenticate.new(@context).build(options)
|
44
|
+
begin
|
45
|
+
Castle::API.request(command).merge(failover: false, failover_reason: nil)
|
46
|
+
rescue Castle::RequestError, Castle::InternalServerError => e
|
47
|
+
self.class.failover_response_or_raise(
|
48
|
+
FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
|
49
|
+
)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
FailoverAuthResponse.new(
|
53
|
+
options[:user_id],
|
54
|
+
strategy: :allow, reason: 'Castle set to do not track.'
|
55
|
+
).generate
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def identify(options = {})
|
60
|
+
options = Castle::Utils.deep_symbolize_keys(options || {})
|
61
|
+
|
62
|
+
return unless tracked?
|
63
|
+
add_timestamp_if_necessary(options)
|
64
|
+
|
65
|
+
command = Castle::Commands::Identify.new(@context).build(options)
|
66
|
+
Castle::API.request(command)
|
67
|
+
end
|
68
|
+
|
69
|
+
def track(options = {})
|
70
|
+
options = Castle::Utils.deep_symbolize_keys(options || {})
|
71
|
+
|
72
|
+
return unless tracked?
|
73
|
+
add_timestamp_if_necessary(options)
|
74
|
+
|
75
|
+
command = Castle::Commands::Track.new(@context).build(options)
|
76
|
+
Castle::API.request(command)
|
77
|
+
end
|
78
|
+
|
79
|
+
def impersonate(options = {})
|
80
|
+
options = Castle::Utils.deep_symbolize_keys(options || {})
|
81
|
+
add_timestamp_if_necessary(options)
|
82
|
+
command = Castle::Commands::Impersonate.new(@context).build(options)
|
83
|
+
Castle::API.request(command).tap do |response|
|
84
|
+
raise Castle::ImpersonationFailed unless response[:success]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def disable_tracking
|
89
|
+
@do_not_track = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def enable_tracking
|
93
|
+
@do_not_track = false
|
94
|
+
end
|
95
|
+
|
96
|
+
def tracked?
|
97
|
+
!@do_not_track
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def add_timestamp_if_necessary(options)
|
103
|
+
options[:timestamp] ||= @timestamp if @timestamp
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|