shipeasy-sdk 1.0.0 → 1.3.0
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 +4 -4
- data/LICENSE +40 -0
- data/README.md +118 -34
- data/lib/shipeasy/config.rb +93 -0
- data/lib/shipeasy/i18n/label_fetcher.rb +66 -0
- data/lib/shipeasy/i18n/railtie.rb +14 -0
- data/lib/shipeasy/i18n/view_helpers.rb +49 -0
- data/lib/shipeasy/sdk/anon_id.rb +47 -0
- data/lib/shipeasy/sdk/eval.rb +6 -1
- data/lib/shipeasy/sdk/flags_client.rb +35 -3
- data/lib/shipeasy/sdk/rack_middleware.rb +85 -0
- data/lib/shipeasy/sdk/railtie.rb +14 -0
- data/lib/shipeasy/sdk/telemetry.rb +78 -0
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy-sdk.rb +17 -1
- metadata +76 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6987470d45b6349d50b279a94bc0fb356812c23f354a743c3c052433dc5759aa
|
|
4
|
+
data.tar.gz: 34d847b481a67fd8876c5e7dfc1351dab1e832fa41f97053324cffb19e24a4c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a3899f6938f09b3e84f0de89f37aab45caca5ae1449157d9e6efd45274a0b224fadbbcfa54505ee84ff760a3b8309fa5545227b0f41cb2650bf83c48d7cfe1f
|
|
7
|
+
data.tar.gz: c5ea14a6d5e53fe124bb8fbb7de06ed3930975b764964698e4c3dbc771f8e68d1766062777fe527f57b8cbd59924b94b864e8f4ed5d7574aa86b066cd9c6d896
|
data/LICENSE
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Shipeasy Source-Available License (Shipeasy-SAL) 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipeasy, Inc. All rights reserved.
|
|
4
|
+
|
|
5
|
+
1. License Grant.
|
|
6
|
+
Subject to the terms of this License, Shipeasy, Inc. ("Shipeasy") grants
|
|
7
|
+
you a non-exclusive, non-transferable, revocable, worldwide license to:
|
|
8
|
+
|
|
9
|
+
(a) Use, copy, and modify the Software solely as a client integration for
|
|
10
|
+
interacting with Shipeasy's hosted services (the "Service");
|
|
11
|
+
(b) Distribute the Software as part of an application that calls the
|
|
12
|
+
Service, in object form, provided the recipient also agrees to this
|
|
13
|
+
License.
|
|
14
|
+
|
|
15
|
+
2. Restrictions.
|
|
16
|
+
You may not:
|
|
17
|
+
|
|
18
|
+
(a) Use the Software, in whole or in part, to build, host, or operate any
|
|
19
|
+
service that competes with the Service or that provides feature-flag,
|
|
20
|
+
experimentation, configuration, internationalization, or related
|
|
21
|
+
functionality to third parties on a commercial basis;
|
|
22
|
+
(b) Sublicense, sell, rent, or lease the Software;
|
|
23
|
+
(c) Remove or alter copyright notices, license terms, or attribution.
|
|
24
|
+
|
|
25
|
+
3. Contributions.
|
|
26
|
+
Any pull request you submit is licensed back to Shipeasy under this
|
|
27
|
+
License plus a perpetual, irrevocable right for Shipeasy to relicense.
|
|
28
|
+
|
|
29
|
+
4. Trademarks.
|
|
30
|
+
This License does not grant rights in the names "Shipeasy", related
|
|
31
|
+
marks, or logos.
|
|
32
|
+
|
|
33
|
+
5. No Warranty / Limitation of Liability.
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. IN NO
|
|
35
|
+
EVENT SHALL SHIPEASY BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
36
|
+
LIABILITY ARISING FROM USE OF THE SOFTWARE.
|
|
37
|
+
|
|
38
|
+
6. Termination.
|
|
39
|
+
This License terminates automatically if you breach it. Sections 2-5
|
|
40
|
+
survive termination.
|
data/README.md
CHANGED
|
@@ -1,61 +1,145 @@
|
|
|
1
|
-
# shipeasy-sdk
|
|
1
|
+
# shipeasy-sdk (Ruby)
|
|
2
2
|
|
|
3
|
-
Ruby
|
|
3
|
+
Ruby gem for the [Shipeasy](https://shipeasy.ai) hosted service. Server-side
|
|
4
|
+
gate evaluation, runtime configs, experiments, and metric ingestion.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
> Source-available under the [Shipeasy-SAL 1.0](./LICENSE).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
6
9
|
|
|
7
10
|
```ruby
|
|
8
11
|
# Gemfile
|
|
9
12
|
gem "shipeasy-sdk"
|
|
10
13
|
```
|
|
11
14
|
|
|
12
|
-
##
|
|
15
|
+
## Quickstart (Rails)
|
|
16
|
+
|
|
17
|
+
`config/initializers/shipeasy.rb` is all you need:
|
|
13
18
|
|
|
14
19
|
```ruby
|
|
15
|
-
|
|
20
|
+
Shipeasy.configure do |c|
|
|
21
|
+
c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY")
|
|
22
|
+
c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY") # for i18n view helpers
|
|
23
|
+
c.profile = "default"
|
|
24
|
+
end
|
|
25
|
+
```
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
client.init # fetches flags/experiments + starts background polling thread
|
|
27
|
+
Anywhere in your app:
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
```ruby
|
|
30
|
+
user = { user_id: current_user.id, plan: current_user.plan }
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# show new checkout
|
|
32
|
+
if Shipeasy.flags.get_flag("new_checkout", user)
|
|
33
|
+
# ship it
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
color = Shipeasy.flags.get_config("button_color")
|
|
37
|
+
result = Shipeasy.flags.get_experiment("checkout_cta", user, { label: "Buy now" })
|
|
38
|
+
Shipeasy.flags.track(current_user.id.to_s, "checkout_completed", { revenue: 49.99 })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`Shipeasy.flags` is a lazy, **fork-safe** singleton: the first call from
|
|
42
|
+
each process spawns its own `FlagsClient` and starts the background poll
|
|
43
|
+
thread, including post-fork Puma workers under `preload_app!`. No need
|
|
44
|
+
for `before_worker_boot` hooks or holding a global constant.
|
|
45
|
+
|
|
46
|
+
In a Rails view (the railtie auto-mounts these helpers when Rails is loaded):
|
|
47
|
+
|
|
48
|
+
```erb
|
|
49
|
+
<%= i18n_head_tags %>
|
|
50
|
+
<h1><%= i18n_t("hero.title", name: current_user.name) %></h1>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Anonymous visitors (zero-config bucketing)
|
|
54
|
+
|
|
55
|
+
For logged-out traffic you need a *stable* unit so a fractional rollout buckets
|
|
56
|
+
the same on the server and in the browser. In Rails this is automatic: a Railtie
|
|
57
|
+
mounts `Shipeasy::SDK::RackMiddleware`, which mints the shared `__se_anon_id`
|
|
58
|
+
first-party cookie (read + written by every Shipeasy SDK, including the browser)
|
|
59
|
+
for any request without one. Evaluations then default to it with **no per-call
|
|
60
|
+
wiring** — `get_flag` on an anonymous request just works:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# current_user is nil → buckets on the __se_anon_id cookie automatically
|
|
64
|
+
Shipeasy.flags.get_flag("new_checkout", {})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
An explicit `user_id` / `anonymous_id` always wins. If you prefer to read the id
|
|
68
|
+
yourself it's also on the Rack env as `request.env["shipeasy.anon_id"]`. The
|
|
69
|
+
cookie is non-`HttpOnly` by design so the browser SDK can bucket identically. A
|
|
70
|
+
request with **no** unit still resolves a fully-rolled (100%) gate as on; only
|
|
71
|
+
fractional gates need the id. Cookie name + format are a cross-SDK contract —
|
|
72
|
+
see `18-identity-bucketing.md`.
|
|
73
|
+
|
|
74
|
+
For **Sinatra / Hanami / bare Rack** (no Railtie), mount it yourself:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
use Shipeasy::SDK::RackMiddleware
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quickstart (plain Ruby / Sinatra / Hanami / scripts)
|
|
29
81
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
puts result.params # { "label" => "Checkout" }
|
|
82
|
+
Same pattern, just without `config/initializers`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
require "shipeasy-sdk"
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
client.track("usr_123", "checkout_completed", { revenue: 49.99 })
|
|
87
|
+
Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY") }
|
|
38
88
|
|
|
39
|
-
|
|
40
|
-
client.destroy
|
|
89
|
+
Shipeasy.flags.get_flag("new_checkout", { user_id: "u_1" })
|
|
41
90
|
```
|
|
42
91
|
|
|
43
|
-
|
|
92
|
+
The Rails view helpers (`i18n_*`) are not loaded outside Rails, so the
|
|
93
|
+
gem doesn't pull Rails into Sinatra/Hanami apps.
|
|
94
|
+
|
|
95
|
+
## Lambda / Cloud Run / serverless
|
|
96
|
+
|
|
97
|
+
Skip the auto-init facade — it spawns a poll thread you don't want in a
|
|
98
|
+
short-lived function. Build the client explicitly and call `init_once`
|
|
99
|
+
for a single synchronous fetch:
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
101
|
+
```ruby
|
|
102
|
+
client = Shipeasy::SDK::FlagsClient.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
|
|
103
|
+
client.init_once
|
|
104
|
+
client.get_flag("new_checkout", user)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Lifecycle escape hatch
|
|
108
|
+
|
|
109
|
+
If you want explicit shutdown control in a long-running worker, build the
|
|
110
|
+
client yourself and skip the singleton:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
client = Shipeasy::SDK.new_client # reads api_key + base_url from Shipeasy.config
|
|
114
|
+
client.init
|
|
115
|
+
at_exit { client.destroy }
|
|
116
|
+
```
|
|
47
117
|
|
|
48
118
|
## Evaluation details
|
|
49
119
|
|
|
50
|
-
- **Gates** — rules matched in order; rollout bucket
|
|
51
|
-
|
|
52
|
-
- **
|
|
53
|
-
|
|
54
|
-
|
|
120
|
+
- **Gates** — rules matched in order; rollout bucket =
|
|
121
|
+
`murmur3("#{salt}:#{uid}") % 10000 < rollout_pct`.
|
|
122
|
+
- **Experiments** — `status == "running"`, optional targeting gate,
|
|
123
|
+
universe holdout range, allocation bucket, then group assignment by
|
|
124
|
+
weight.
|
|
125
|
+
- **MurmurHash3** — pure-Ruby x86_32 variant, seed 0.
|
|
126
|
+
- **ETag caching** — each poll sends `If-None-Match`; a 304 skips the
|
|
127
|
+
JSON parse.
|
|
128
|
+
- **Poll interval** — defaults to 30 s; overridden by the
|
|
129
|
+
`X-Poll-Interval` header from the flags endpoint.
|
|
55
130
|
|
|
56
131
|
## Configuration
|
|
57
132
|
|
|
58
|
-
| Parameter | Default
|
|
59
|
-
|
|
60
|
-
| `api_key` | (required)
|
|
61
|
-
| `base_url` | `https://
|
|
133
|
+
| Parameter | Default | Description |
|
|
134
|
+
| ---------- | ------------------------- | ----------------------------------- |
|
|
135
|
+
| `api_key` | (required) | SDK key from the Shipeasy dashboard |
|
|
136
|
+
| `base_url` | `https://cdn.shipeasy.ai` | Override for local dev / staging |
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
[docs.shipeasy.ai](https://docs.shipeasy.ai)
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
[Shipeasy-SAL 1.0](./LICENSE) — source-available, non-commercial-use,
|
|
145
|
+
permitted as a Shipeasy client.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Single configuration object for the Shipeasy gem.
|
|
2
|
+
#
|
|
3
|
+
# Covers both subsystems:
|
|
4
|
+
# - SDK / experimentation (api_key, base_url) — drives FlagsClient
|
|
5
|
+
# - i18n / string manager (public_key, profile, cdn_base_url, ...) — drives
|
|
6
|
+
# the Rails view helpers and label fetcher
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
#
|
|
10
|
+
# Shipeasy.configure do |c|
|
|
11
|
+
# c.api_key = ENV["SHIPEASY_SERVER_KEY"]
|
|
12
|
+
# c.public_key = ENV["SHIPEASY_CLIENT_KEY"]
|
|
13
|
+
# c.profile = "default"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Anything not set falls back to the defaults below. The same Shipeasy.config
|
|
17
|
+
# is read by FlagsClient and the Rails helpers, so there is one place to
|
|
18
|
+
# point environment variables at.
|
|
19
|
+
|
|
20
|
+
module Shipeasy
|
|
21
|
+
class Configuration
|
|
22
|
+
# ---- experimentation / SDK ----
|
|
23
|
+
attr_accessor :api_key, :base_url
|
|
24
|
+
|
|
25
|
+
# ---- i18n / string manager ----
|
|
26
|
+
attr_accessor :public_key, :profile, :default_chunk,
|
|
27
|
+
:cdn_base_url, :loader_url,
|
|
28
|
+
:manifest_cache_ttl, :label_file_cache_ttl, :http_timeout
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@base_url = "https://edge.shipeasy.dev"
|
|
32
|
+
|
|
33
|
+
@profile = "default"
|
|
34
|
+
@default_chunk = "index"
|
|
35
|
+
@cdn_base_url = "https://cdn.i18n.shipeasy.ai"
|
|
36
|
+
@loader_url = "https://cdn.i18n.shipeasy.ai/loader.js"
|
|
37
|
+
@manifest_cache_ttl = 60
|
|
38
|
+
@label_file_cache_ttl = 3600
|
|
39
|
+
@http_timeout = 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def config
|
|
45
|
+
@config ||= Configuration.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure
|
|
49
|
+
yield config
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reset the config back to defaults — primarily for tests.
|
|
53
|
+
def reset_config!
|
|
54
|
+
@config = nil
|
|
55
|
+
@flags_pid = nil
|
|
56
|
+
@flags&.destroy
|
|
57
|
+
@flags = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Lazy, fork-safe singleton FlagsClient. The first call from each
|
|
61
|
+
# process spawns a fresh client + poll thread — including post-fork
|
|
62
|
+
# workers under Puma's preload_app!. Callers can `Shipeasy.flags.get_flag(...)`
|
|
63
|
+
# straight from a controller without holding a constant or worrying
|
|
64
|
+
# about `before_worker_boot` hooks.
|
|
65
|
+
#
|
|
66
|
+
# Initializers stay minimal:
|
|
67
|
+
#
|
|
68
|
+
# # config/initializers/shipeasy.rb
|
|
69
|
+
# Shipeasy.configure { |c| c.api_key = ENV["SHIPEASY_SERVER_KEY"] }
|
|
70
|
+
#
|
|
71
|
+
# The first request that touches `Shipeasy.flags.*` triggers init().
|
|
72
|
+
# For serverless / Lambda where you want a single fetch with no thread,
|
|
73
|
+
# build the client explicitly: `Shipeasy::SDK::FlagsClient.new(...).init_once`.
|
|
74
|
+
def flags
|
|
75
|
+
pid = Process.pid
|
|
76
|
+
if @flags && @flags_pid != pid
|
|
77
|
+
# Post-fork: parent's poll thread didn't survive. Don't destroy
|
|
78
|
+
# @flags (its mutex/state is invalid in this child anyway); just
|
|
79
|
+
# rebuild from scratch.
|
|
80
|
+
@flags = nil
|
|
81
|
+
end
|
|
82
|
+
@flags ||= begin
|
|
83
|
+
@flags_pid = pid
|
|
84
|
+
client = SDK::FlagsClient.new(
|
|
85
|
+
api_key: config.api_key,
|
|
86
|
+
base_url: config.base_url,
|
|
87
|
+
)
|
|
88
|
+
client.init
|
|
89
|
+
client
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Shipeasy
|
|
7
|
+
module I18n
|
|
8
|
+
class LabelFetcher
|
|
9
|
+
MANIFEST_KEY_PREFIX = "i18n:manifest:"
|
|
10
|
+
LABEL_KEY_PREFIX = "i18n:label:"
|
|
11
|
+
|
|
12
|
+
def initialize(config = Shipeasy.config)
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(profile: @config.profile, chunk: @config.default_chunk)
|
|
17
|
+
manifest = fetch_manifest(profile)
|
|
18
|
+
return nil unless manifest
|
|
19
|
+
|
|
20
|
+
file_url = manifest[chunk]
|
|
21
|
+
return nil unless file_url
|
|
22
|
+
|
|
23
|
+
fetch_label_file(file_url)
|
|
24
|
+
rescue => e
|
|
25
|
+
::Rails.logger.warn("[Shipeasy::I18n] Failed to fetch labels: #{e.message}") if defined?(::Rails)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def fetch_manifest(profile)
|
|
32
|
+
cache_key = "#{MANIFEST_KEY_PREFIX}#{@config.public_key}:#{profile}"
|
|
33
|
+
cache_fetch(cache_key, @config.manifest_cache_ttl) do
|
|
34
|
+
url = "#{@config.cdn_base_url}/labels/#{@config.public_key}/#{profile}/manifest.json"
|
|
35
|
+
http_get_json(url)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_label_file(url)
|
|
40
|
+
cache_key = "#{LABEL_KEY_PREFIX}#{Digest::MD5.hexdigest(url)}"
|
|
41
|
+
cache_fetch(cache_key, @config.label_file_cache_ttl) do
|
|
42
|
+
http_get_json(url)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cache_fetch(key, ttl, &block)
|
|
47
|
+
if defined?(::Rails) && ::Rails.cache
|
|
48
|
+
::Rails.cache.fetch(key, expires_in: ttl.seconds, &block)
|
|
49
|
+
else
|
|
50
|
+
block.call
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def http_get_json(url)
|
|
55
|
+
uri = URI.parse(url)
|
|
56
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
57
|
+
http.use_ssl = (uri.scheme == "https")
|
|
58
|
+
http.open_timeout = @config.http_timeout
|
|
59
|
+
http.read_timeout = @config.http_timeout
|
|
60
|
+
res = http.get(uri.request_uri, { "Accept" => "application/json" })
|
|
61
|
+
raise "HTTP #{res.code} fetching #{url}" unless res.is_a?(Net::HTTPSuccess)
|
|
62
|
+
JSON.parse(res.body)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Shipeasy
|
|
2
|
+
module I18n
|
|
3
|
+
# Auto-mounts ViewHelpers into ActionView when the gem is loaded inside
|
|
4
|
+
# a Rails app. Skipped silently when ::Rails isn't defined (plain Ruby
|
|
5
|
+
# consumers of the SDK never see the i18n surface).
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer "shipeasy.i18n.view_helpers" do
|
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
|
9
|
+
include Shipeasy::I18n::ViewHelpers
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Shipeasy
|
|
2
|
+
module I18n
|
|
3
|
+
module ViewHelpers
|
|
4
|
+
def i18n_head_tags(profile: nil, chunk: nil)
|
|
5
|
+
safe_join([
|
|
6
|
+
i18n_inline_data(profile: profile, chunk: chunk),
|
|
7
|
+
i18n_script_tag,
|
|
8
|
+
], "\n")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def i18n_inline_data(profile: nil, chunk: nil)
|
|
12
|
+
config = Shipeasy.config
|
|
13
|
+
label_file = Shipeasy::I18n::LabelFetcher.new.fetch(
|
|
14
|
+
profile: profile || config.profile,
|
|
15
|
+
chunk: chunk || config.default_chunk,
|
|
16
|
+
)
|
|
17
|
+
return "".html_safe unless label_file
|
|
18
|
+
|
|
19
|
+
json_content = JSON.generate(label_file)
|
|
20
|
+
content_tag(:script, json_content.html_safe, id: "i18n-data", type: "application/json")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def i18n_script_tag(hide_until_ready: false)
|
|
24
|
+
config = Shipeasy.config
|
|
25
|
+
attrs = {
|
|
26
|
+
src: config.loader_url,
|
|
27
|
+
"data-key": config.public_key,
|
|
28
|
+
"data-profile": config.profile,
|
|
29
|
+
async: true,
|
|
30
|
+
}
|
|
31
|
+
attrs[:"data-hide-until-ready"] = "true" if hide_until_ready
|
|
32
|
+
tag(:script, attrs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def i18n_t(key, variables = {}, profile: nil, chunk: nil)
|
|
36
|
+
config = Shipeasy.config
|
|
37
|
+
label_file = Shipeasy::I18n::LabelFetcher.new.fetch(
|
|
38
|
+
profile: profile || config.profile,
|
|
39
|
+
chunk: chunk || config.default_chunk,
|
|
40
|
+
)
|
|
41
|
+
return key unless label_file && label_file["strings"]
|
|
42
|
+
|
|
43
|
+
value = label_file["strings"][key] || key
|
|
44
|
+
variables.each { |k, v| value = value.gsub("{{#{k}}}", v.to_s) }
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Shipeasy
|
|
4
|
+
module SDK
|
|
5
|
+
# Anonymous bucketing identity — the cross-SDK `__se_anon_id` cookie.
|
|
6
|
+
#
|
|
7
|
+
# Gates and experiments bucket a unit with murmur3(salt:unit). For a
|
|
8
|
+
# logged-out visitor the unit is a stable anonymous id carried in a single
|
|
9
|
+
# first-party cookie that EVERY Shipeasy SDK (server + browser) reads and
|
|
10
|
+
# writes, so a server render and the browser bucket a fractional rollout
|
|
11
|
+
# identically. The cookie name + format are frozen across every language;
|
|
12
|
+
# see experiment-platform/18-identity-bucketing.md.
|
|
13
|
+
module AnonId
|
|
14
|
+
COOKIE = "__se_anon_id".freeze
|
|
15
|
+
MAX_AGE = 31_536_000 # 1 year, in seconds
|
|
16
|
+
|
|
17
|
+
# The cookie value is client-controllable and feeds bucketing, so a
|
|
18
|
+
# tampered value is treated as absent and a fresh id is minted. UUIDs
|
|
19
|
+
# satisfy this charset.
|
|
20
|
+
VALID_RX = /\A[A-Za-z0-9_-]{1,64}\z/.freeze
|
|
21
|
+
|
|
22
|
+
THREAD_KEY = :shipeasy_anon_id
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# A fresh opaque bucketing id (UUIDv4).
|
|
27
|
+
def mint
|
|
28
|
+
SecureRandom.uuid
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid?(value)
|
|
32
|
+
value.is_a?(String) && VALID_RX.match?(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The anon id RackMiddleware resolved for the current request, or nil when
|
|
36
|
+
# no middleware ran (e.g. a background job). FlagsClient falls back to this
|
|
37
|
+
# as the default anonymous_id, so evaluations need no per-call wiring.
|
|
38
|
+
def current
|
|
39
|
+
Thread.current[THREAD_KEY]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def current=(value)
|
|
43
|
+
Thread.current[THREAD_KEY] = value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/shipeasy/sdk/eval.rb
CHANGED
|
@@ -66,7 +66,12 @@ module Shipeasy
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
uid = user["user_id"] || user[:user_id] || user["anonymous_id"] || user[:anonymous_id]
|
|
69
|
-
|
|
69
|
+
# No unit id (an unidentified request before any anon id is minted): a
|
|
70
|
+
# fully-rolled gate is on for everyone, so it can be answered without
|
|
71
|
+
# bucketing; a fractional rollout genuinely needs a stable unit, so deny
|
|
72
|
+
# until one exists. Rules above are still checked, so targeting wins.
|
|
73
|
+
# See experiment-platform/18-identity-bucketing.md.
|
|
74
|
+
return (gate["rolloutPct"] || gate[:rolloutPct] || 0) >= 10000 unless uid
|
|
70
75
|
|
|
71
76
|
salt = gate["salt"] || gate[:salt]
|
|
72
77
|
murmur3("#{salt}:#{uid}") % 10000 < (gate["rolloutPct"] || gate[:rolloutPct] || 0)
|
|
@@ -3,15 +3,26 @@ require "uri"
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "thread"
|
|
5
5
|
require_relative "eval"
|
|
6
|
+
require_relative "telemetry"
|
|
7
|
+
require_relative "anon_id"
|
|
6
8
|
|
|
7
9
|
module Shipeasy
|
|
8
10
|
module SDK
|
|
9
11
|
class FlagsClient
|
|
10
12
|
DEFAULT_BASE_URL = "https://edge.shipeasy.dev"
|
|
11
13
|
|
|
12
|
-
def initialize(api_key:, base_url: nil)
|
|
14
|
+
def initialize(api_key:, base_url: nil, env: "prod", disable_telemetry: false, telemetry_url: nil)
|
|
13
15
|
@api_key = api_key
|
|
14
16
|
@base_url = (base_url || DEFAULT_BASE_URL).chomp("/")
|
|
17
|
+
# Per-evaluation usage telemetry. ON by default; pass
|
|
18
|
+
# disable_telemetry: true to opt out. See telemetry.rb.
|
|
19
|
+
@telemetry = Telemetry.new(
|
|
20
|
+
endpoint: telemetry_url || Telemetry::DEFAULT_TELEMETRY_URL,
|
|
21
|
+
sdk_key: api_key,
|
|
22
|
+
side: "server",
|
|
23
|
+
env: env,
|
|
24
|
+
disabled: disable_telemetry,
|
|
25
|
+
)
|
|
15
26
|
@flags_blob = nil
|
|
16
27
|
@exps_blob = nil
|
|
17
28
|
@flags_etag = nil
|
|
@@ -40,12 +51,14 @@ module Shipeasy
|
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
def get_flag(name, user)
|
|
54
|
+
@telemetry.emit("gate", name)
|
|
43
55
|
gate = @mutex.synchronize { @flags_blob&.dig("gates", name) }
|
|
44
56
|
return false unless gate
|
|
45
|
-
Eval.eval_gate(gate, user
|
|
57
|
+
Eval.eval_gate(gate, with_anon_id(user))
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
def get_config(name, decode = nil)
|
|
61
|
+
@telemetry.emit("config", name)
|
|
49
62
|
entry = @mutex.synchronize { @flags_blob&.dig("configs", name) }
|
|
50
63
|
return nil unless entry
|
|
51
64
|
value = entry["value"]
|
|
@@ -53,9 +66,10 @@ module Shipeasy
|
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
def get_experiment(name, user, default_params, decode = nil)
|
|
69
|
+
@telemetry.emit("experiment", name)
|
|
56
70
|
flags_blob, exps_blob = @mutex.synchronize { [@flags_blob, @exps_blob] }
|
|
57
71
|
exp = exps_blob&.dig("experiments", name)
|
|
58
|
-
result = Eval.eval_experiment(exp, flags_blob, exps_blob, user
|
|
72
|
+
result = Eval.eval_experiment(exp, flags_blob, exps_blob, with_anon_id(user))
|
|
59
73
|
result.params ||= default_params
|
|
60
74
|
|
|
61
75
|
if result.in_experiment && decode
|
|
@@ -94,6 +108,24 @@ module Shipeasy
|
|
|
94
108
|
|
|
95
109
|
private
|
|
96
110
|
|
|
111
|
+
# Normalise the user hash to string keys and, when the caller passed no
|
|
112
|
+
# explicit unit, default anonymous_id to the request's __se_anon_id (set by
|
|
113
|
+
# RackMiddleware). Lets `get_flag("x", {})` bucket anonymous traffic with
|
|
114
|
+
# zero per-call wiring. A caller-supplied user_id/anonymous_id always wins.
|
|
115
|
+
def with_anon_id(user)
|
|
116
|
+
u = user.transform_keys(&:to_s)
|
|
117
|
+
has_unit = !blank?(u["user_id"]) || !blank?(u["anonymous_id"])
|
|
118
|
+
unless has_unit
|
|
119
|
+
anon = AnonId.current
|
|
120
|
+
u["anonymous_id"] = anon if anon
|
|
121
|
+
end
|
|
122
|
+
u
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def blank?(v)
|
|
126
|
+
v.nil? || v == ""
|
|
127
|
+
end
|
|
128
|
+
|
|
97
129
|
def start_poll
|
|
98
130
|
@timer = Thread.new do
|
|
99
131
|
loop do
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative "anon_id"
|
|
2
|
+
|
|
3
|
+
module Shipeasy
|
|
4
|
+
module SDK
|
|
5
|
+
# Rack middleware that mints the shared `__se_anon_id` bucketing cookie.
|
|
6
|
+
#
|
|
7
|
+
# For every request without a valid `__se_anon_id` cookie it mints a UUIDv4,
|
|
8
|
+
# exposes it for the duration of the request, and Set-Cookies it on the
|
|
9
|
+
# response. Once installed, gate/experiment evaluations with no explicit
|
|
10
|
+
# user_id / anonymous_id automatically bucket on the cookie id — anonymous
|
|
11
|
+
# visitors get stable, SSR/browser-consistent bucketing with zero per-call
|
|
12
|
+
# wiring.
|
|
13
|
+
#
|
|
14
|
+
# Rails apps get this automatically (a Railtie inserts it). For Sinatra /
|
|
15
|
+
# Hanami / bare Rack, add it yourself:
|
|
16
|
+
#
|
|
17
|
+
# use Shipeasy::SDK::RackMiddleware
|
|
18
|
+
#
|
|
19
|
+
# The resolved id is also stored in the Rack env under "shipeasy.anon_id"
|
|
20
|
+
# for callers that prefer to read it explicitly.
|
|
21
|
+
class RackMiddleware
|
|
22
|
+
ENV_KEY = "shipeasy.anon_id".freeze
|
|
23
|
+
|
|
24
|
+
def initialize(app)
|
|
25
|
+
@app = app
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(env)
|
|
29
|
+
id, minted = read_or_mint(env)
|
|
30
|
+
env[ENV_KEY] = id
|
|
31
|
+
AnonId.current = id
|
|
32
|
+
begin
|
|
33
|
+
status, headers, body = @app.call(env)
|
|
34
|
+
ensure
|
|
35
|
+
# Don't leak the id onto the next request handled by this thread.
|
|
36
|
+
AnonId.current = nil
|
|
37
|
+
end
|
|
38
|
+
set_cookie!(headers, id, env) if minted
|
|
39
|
+
[status, headers, body]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def read_or_mint(env)
|
|
45
|
+
raw = parse_cookies(env["HTTP_COOKIE"])[AnonId::COOKIE]
|
|
46
|
+
return [raw, false] if AnonId.valid?(raw)
|
|
47
|
+
|
|
48
|
+
[AnonId.mint, true]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_cookies(header)
|
|
52
|
+
out = {}
|
|
53
|
+
return out unless header
|
|
54
|
+
|
|
55
|
+
header.split(/;\s*/).each do |pair|
|
|
56
|
+
k, v = pair.split("=", 2)
|
|
57
|
+
out[k] = v if k && v && !out.key?(k)
|
|
58
|
+
end
|
|
59
|
+
out
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_cookie!(headers, id, env)
|
|
63
|
+
cookie = +"#{AnonId::COOKIE}=#{id}; Path=/; Max-Age=#{AnonId::MAX_AGE}; SameSite=Lax"
|
|
64
|
+
cookie << "; Secure" if https?(env)
|
|
65
|
+
|
|
66
|
+
# Append without clobbering any Set-Cookie the app already emitted, and
|
|
67
|
+
# match the existing header key's case (Rack 3 mandates lowercase).
|
|
68
|
+
key = headers.keys.find { |k| k.respond_to?(:casecmp) && k.casecmp("set-cookie").zero? } || "Set-Cookie"
|
|
69
|
+
existing = headers[key]
|
|
70
|
+
headers[key] =
|
|
71
|
+
case existing
|
|
72
|
+
when nil then cookie
|
|
73
|
+
when Array then existing + [cookie]
|
|
74
|
+
else "#{existing}\n#{cookie}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def https?(env)
|
|
79
|
+
env["HTTPS"] == "on" ||
|
|
80
|
+
env["rack.url_scheme"] == "https" ||
|
|
81
|
+
env["HTTP_X_FORWARDED_PROTO"].to_s.split(",").first.to_s.strip.casecmp("https").zero?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require_relative "rack_middleware"
|
|
2
|
+
|
|
3
|
+
module Shipeasy
|
|
4
|
+
module SDK
|
|
5
|
+
# Auto-mounts RackMiddleware in a Rails app so anonymous bucketing works
|
|
6
|
+
# out of the box — no manual `config.middleware.use`. Loaded only when Rails
|
|
7
|
+
# is present (see lib/shipeasy-sdk.rb), so plain Ruby apps are unaffected.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
initializer "shipeasy.sdk.anon_id_middleware" do |app|
|
|
10
|
+
app.middleware.use Shipeasy::SDK::RackMiddleware
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "digest"
|
|
4
|
+
require "erb"
|
|
5
|
+
require "thread"
|
|
6
|
+
|
|
7
|
+
module Shipeasy
|
|
8
|
+
module SDK
|
|
9
|
+
# Per-evaluation usage telemetry. Fires one fire-and-forget HTTP beacon per
|
|
10
|
+
# evaluation so usage is counted by Cloudflare's native per-path analytics.
|
|
11
|
+
# Mirrors the contract in the TypeScript reference SDK and
|
|
12
|
+
# experiment-platform/15-usage-metering.md. The path carries sha256(api_key)
|
|
13
|
+
# -- never the raw key -- plus side/env, then feature/resource. A long-lived
|
|
14
|
+
# Ruby process emits reliably; the 2s dedup window bounds volume under loops.
|
|
15
|
+
class Telemetry
|
|
16
|
+
DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai"
|
|
17
|
+
|
|
18
|
+
def initialize(endpoint:, sdk_key:, side: "server", env: "prod", disabled: false, dedupe_ms: 2000)
|
|
19
|
+
endpoint = (endpoint || "").chomp("/")
|
|
20
|
+
@disabled = disabled || sdk_key.nil? || sdk_key.empty? || endpoint.empty?
|
|
21
|
+
@dedupe_ms = dedupe_ms
|
|
22
|
+
@last = {}
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
unless @disabled
|
|
25
|
+
key_hash = Digest::SHA256.hexdigest(sdk_key)
|
|
26
|
+
@prefix = "#{endpoint}/t/#{key_hash}/#{side}/#{enc(env)}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Best-effort usage beacon for one evaluation. Never blocks the caller
|
|
31
|
+
# (the thread owns the request) and never raises into evaluation.
|
|
32
|
+
def emit(feature, resource)
|
|
33
|
+
return if @disabled
|
|
34
|
+
|
|
35
|
+
if @dedupe_ms > 0
|
|
36
|
+
dedupe_key = "#{feature}/#{resource}"
|
|
37
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0
|
|
38
|
+
duplicate = @mutex.synchronize do
|
|
39
|
+
last = @last[dedupe_key]
|
|
40
|
+
if last && (now - last) < @dedupe_ms
|
|
41
|
+
true
|
|
42
|
+
else
|
|
43
|
+
@last[dedupe_key] = now
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
return if duplicate
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
dispatch("#{@prefix}/#{feature}/#{enc(resource)}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Fire-and-forget HTTP GET on a background thread. Isolated as its own
|
|
56
|
+
# method so tests can intercept it without real network/timing.
|
|
57
|
+
def dispatch(url)
|
|
58
|
+
Thread.new do
|
|
59
|
+
begin
|
|
60
|
+
uri = URI(url)
|
|
61
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
62
|
+
http.use_ssl = uri.scheme == "https"
|
|
63
|
+
http.open_timeout = 2
|
|
64
|
+
http.read_timeout = 2
|
|
65
|
+
http.get(uri.request_uri)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
# telemetry must never affect the caller
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# encodeURIComponent-equivalent: %20 for space, %2F for slash (NOT "+").
|
|
73
|
+
def enc(value)
|
|
74
|
+
ERB::Util.url_encode(value.to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/shipeasy/sdk/version.rb
CHANGED
data/lib/shipeasy-sdk.rb
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
require_relative "shipeasy/sdk/version"
|
|
2
|
+
require_relative "shipeasy/config"
|
|
2
3
|
require_relative "shipeasy/sdk/murmur3"
|
|
3
4
|
require_relative "shipeasy/sdk/eval"
|
|
4
5
|
require_relative "shipeasy/sdk/flags_client"
|
|
6
|
+
require_relative "shipeasy/sdk/anon_id"
|
|
7
|
+
require_relative "shipeasy/sdk/rack_middleware"
|
|
8
|
+
require_relative "shipeasy/i18n/label_fetcher"
|
|
9
|
+
|
|
10
|
+
# Rails-only surface. Skipped on plain Ruby so the gem stays usable in
|
|
11
|
+
# non-Rails apps (Sinatra, Hanami, scripts) without pulling Rails in.
|
|
12
|
+
if defined?(::Rails)
|
|
13
|
+
require_relative "shipeasy/i18n/view_helpers"
|
|
14
|
+
require_relative "shipeasy/i18n/railtie"
|
|
15
|
+
# Auto-mounts RackMiddleware so anonymous bucketing works with no config.
|
|
16
|
+
require_relative "shipeasy/sdk/railtie"
|
|
17
|
+
end
|
|
5
18
|
|
|
6
19
|
module Shipeasy
|
|
7
20
|
module SDK
|
|
8
|
-
|
|
21
|
+
# Convenience constructor. Reads api_key + base_url from the gem-wide
|
|
22
|
+
# config when omitted, so a single `Shipeasy.configure { … }` block at
|
|
23
|
+
# boot is enough.
|
|
24
|
+
def self.new_client(api_key: Shipeasy.config.api_key, base_url: Shipeasy.config.base_url)
|
|
9
25
|
FlagsClient.new(api_key: api_key, base_url: base_url)
|
|
10
26
|
end
|
|
11
27
|
end
|
metadata
CHANGED
|
@@ -1,32 +1,93 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shipeasy-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Shipeasy, Inc.
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rubocop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.71'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.71'
|
|
55
|
+
description: Server SDK for Shipeasy. Polls /sdk/flags and /sdk/experiments, evaluates
|
|
56
|
+
gates and experiments locally, forwards exposures + metrics to /collect, and (when
|
|
57
|
+
loaded inside Rails) auto-mounts i18n_head_tags / i18n_inline_data / i18n_script_tag
|
|
58
|
+
/ i18n_t view helpers for the Shipeasy string-manager CDN.
|
|
14
59
|
email:
|
|
15
|
-
- sdk@shipeasy.
|
|
60
|
+
- sdk@shipeasy.ai
|
|
16
61
|
executables: []
|
|
17
62
|
extensions: []
|
|
18
63
|
extra_rdoc_files: []
|
|
19
64
|
files:
|
|
65
|
+
- LICENSE
|
|
20
66
|
- README.md
|
|
21
67
|
- lib/shipeasy-sdk.rb
|
|
68
|
+
- lib/shipeasy/config.rb
|
|
69
|
+
- lib/shipeasy/i18n/label_fetcher.rb
|
|
70
|
+
- lib/shipeasy/i18n/railtie.rb
|
|
71
|
+
- lib/shipeasy/i18n/view_helpers.rb
|
|
72
|
+
- lib/shipeasy/sdk/anon_id.rb
|
|
22
73
|
- lib/shipeasy/sdk/eval.rb
|
|
23
74
|
- lib/shipeasy/sdk/flags_client.rb
|
|
24
75
|
- lib/shipeasy/sdk/murmur3.rb
|
|
76
|
+
- lib/shipeasy/sdk/rack_middleware.rb
|
|
77
|
+
- lib/shipeasy/sdk/railtie.rb
|
|
78
|
+
- lib/shipeasy/sdk/telemetry.rb
|
|
25
79
|
- lib/shipeasy/sdk/version.rb
|
|
26
|
-
homepage: https://github.com/shipeasy/sdk-ruby
|
|
80
|
+
homepage: https://github.com/shipeasy-ai/sdk-ruby
|
|
27
81
|
licenses:
|
|
28
|
-
-
|
|
29
|
-
metadata:
|
|
82
|
+
- Nonstandard
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://github.com/shipeasy-ai/sdk-ruby
|
|
85
|
+
source_code_uri: https://github.com/shipeasy-ai/sdk-ruby
|
|
86
|
+
bug_tracker_uri: https://github.com/shipeasy-ai/sdk-ruby/issues
|
|
87
|
+
documentation_uri: https://docs.shipeasy.ai
|
|
88
|
+
license_file: LICENSE
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
90
|
+
post_install_message:
|
|
30
91
|
rdoc_options: []
|
|
31
92
|
require_paths:
|
|
32
93
|
- lib
|
|
@@ -34,14 +95,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
34
95
|
requirements:
|
|
35
96
|
- - ">="
|
|
36
97
|
- !ruby/object:Gem::Version
|
|
37
|
-
version:
|
|
98
|
+
version: '3.0'
|
|
38
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
100
|
requirements:
|
|
40
101
|
- - ">="
|
|
41
102
|
- !ruby/object:Gem::Version
|
|
42
103
|
version: '0'
|
|
43
104
|
requirements: []
|
|
44
|
-
rubygems_version:
|
|
105
|
+
rubygems_version: 3.5.22
|
|
106
|
+
signing_key:
|
|
45
107
|
specification_version: 4
|
|
46
|
-
summary:
|
|
108
|
+
summary: Shipeasy feature gates, runtime configs, experiments, metrics, and i18n helpers
|
|
109
|
+
— Ruby gem.
|
|
47
110
|
test_files: []
|