shipeasy-sdk 2.0.0 → 2.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/README.md +87 -294
- data/bin/shipeasy-skill +8 -0
- data/docs/skill/SKILL.md +152 -0
- data/lib/generators/shipeasy/install/USAGE +24 -0
- data/lib/generators/shipeasy/install/install_generator.rb +86 -0
- data/lib/generators/shipeasy/install/templates/initializer.rb.tt +41 -0
- data/lib/shipeasy/admin.rb +85 -0
- data/lib/shipeasy/client.rb +9 -0
- data/lib/shipeasy/config.rb +192 -7
- data/lib/shipeasy/engine.rb +16 -8
- data/lib/shipeasy/sdk/openfeature.rb +33 -6
- data/lib/shipeasy/sdk/skill.rb +76 -0
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy_admin/api/alert_rules_api.rb +292 -0
- data/lib/shipeasy_admin/api/attributes_api.rb +82 -0
- data/lib/shipeasy_admin/api/configs_api.rb +680 -0
- data/lib/shipeasy_admin/api/events_api.rb +438 -0
- data/lib/shipeasy_admin/api/experiments_api.rb +727 -0
- data/lib/shipeasy_admin/api/gates_api.rb +438 -0
- data/lib/shipeasy_admin/api/i18n_api.rb +507 -0
- data/lib/shipeasy_admin/api/killswitches_api.rb +526 -0
- data/lib/shipeasy_admin/api/metrics_api.rb +281 -0
- data/lib/shipeasy_admin/api/ops_api.rb +525 -0
- data/lib/shipeasy_admin/api/projects_api.rb +153 -0
- data/lib/shipeasy_admin/api/universes_api.rb +306 -0
- data/lib/shipeasy_admin/api_client.rb +441 -0
- data/lib/shipeasy_admin/api_error.rb +58 -0
- data/lib/shipeasy_admin/api_model_base.rb +88 -0
- data/lib/shipeasy_admin/configuration.rb +397 -0
- data/lib/shipeasy_admin/models/approve_event_request.rb +198 -0
- data/lib/shipeasy_admin/models/approve_event_response.rb +166 -0
- data/lib/shipeasy_admin/models/create_alert_rule_request.rb +408 -0
- data/lib/shipeasy_admin/models/create_alert_rule_response.rb +165 -0
- data/lib/shipeasy_admin/models/create_config_request.rb +280 -0
- data/lib/shipeasy_admin/models/create_config_response.rb +201 -0
- data/lib/shipeasy_admin/models/create_event_request.rb +253 -0
- data/lib/shipeasy_admin/models/create_event_response.rb +193 -0
- data/lib/shipeasy_admin/models/create_experiment_request.rb +869 -0
- data/lib/shipeasy_admin/models/create_experiment_request_goal_metric.rb +336 -0
- data/lib/shipeasy_admin/models/create_experiment_response.rb +212 -0
- data/lib/shipeasy_admin/models/create_gate_request.rb +529 -0
- data/lib/shipeasy_admin/models/create_gate_response.rb +212 -0
- data/lib/shipeasy_admin/models/create_i18n_profile_request.rb +188 -0
- data/lib/shipeasy_admin/models/create_i18n_profile_response.rb +193 -0
- data/lib/shipeasy_admin/models/create_killswitch_request.rb +263 -0
- data/lib/shipeasy_admin/models/create_killswitch_response.rb +201 -0
- data/lib/shipeasy_admin/models/create_metric_request.rb +415 -0
- data/lib/shipeasy_admin/models/create_metric_response.rb +193 -0
- data/lib/shipeasy_admin/models/create_ops_item_request.rb +287 -0
- data/lib/shipeasy_admin/models/create_ops_item_response.rb +177 -0
- data/lib/shipeasy_admin/models/create_universe_request.rb +289 -0
- data/lib/shipeasy_admin/models/create_universe_response.rb +212 -0
- data/lib/shipeasy_admin/models/delete_alert_rule_response.rb +188 -0
- data/lib/shipeasy_admin/models/delete_config_response.rb +188 -0
- data/lib/shipeasy_admin/models/delete_event_response.rb +189 -0
- data/lib/shipeasy_admin/models/delete_experiment_response.rb +188 -0
- data/lib/shipeasy_admin/models/delete_gate_response.rb +188 -0
- data/lib/shipeasy_admin/models/delete_killswitch_response.rb +188 -0
- data/lib/shipeasy_admin/models/delete_metric_response.rb +189 -0
- data/lib/shipeasy_admin/models/delete_universe_response.rb +188 -0
- data/lib/shipeasy_admin/models/disable_gate_response.rb +190 -0
- data/lib/shipeasy_admin/models/discard_config_draft_request.rb +190 -0
- data/lib/shipeasy_admin/models/discard_config_draft_response.rb +188 -0
- data/lib/shipeasy_admin/models/enable_gate_response.rb +190 -0
- data/lib/shipeasy_admin/models/error_response.rb +185 -0
- data/lib/shipeasy_admin/models/get_config_response.rb +351 -0
- data/lib/shipeasy_admin/models/get_current_project_response.rb +606 -0
- data/lib/shipeasy_admin/models/get_current_project_response_module_translations.rb +104 -0
- data/lib/shipeasy_admin/models/get_event_response.rb +344 -0
- data/lib/shipeasy_admin/models/get_experiment_response.rb +688 -0
- data/lib/shipeasy_admin/models/get_experiment_results_response.rb +243 -0
- data/lib/shipeasy_admin/models/get_experiment_results_response_experiment.rb +248 -0
- data/lib/shipeasy_admin/models/get_experiment_results_response_results_inner.rb +252 -0
- data/lib/shipeasy_admin/models/get_experiment_timeseries_response.rb +192 -0
- data/lib/shipeasy_admin/models/get_experiment_timeseries_response_experiment.rb +261 -0
- data/lib/shipeasy_admin/models/get_experiment_timeseries_response_series_inner.rb +278 -0
- data/lib/shipeasy_admin/models/get_killswitch_response.rb +270 -0
- data/lib/shipeasy_admin/models/get_metric_response.rb +378 -0
- data/lib/shipeasy_admin/models/get_ops_item_response.rb +349 -0
- data/lib/shipeasy_admin/models/link_pr_to_ops_item_request.rb +186 -0
- data/lib/shipeasy_admin/models/link_pr_to_ops_item_response.rb +166 -0
- data/lib/shipeasy_admin/models/list_alert_rules_response_inner.rb +478 -0
- data/lib/shipeasy_admin/models/list_alert_rules_response_inner_notify.rb +177 -0
- data/lib/shipeasy_admin/models/list_alert_rules_response_inner_notify_slack_channel.rb +211 -0
- data/lib/shipeasy_admin/models/list_attributes_response_inner.rb +176 -0
- data/lib/shipeasy_admin/models/list_config_activity_response_inner.rb +280 -0
- data/lib/shipeasy_admin/models/list_configs_response.rb +178 -0
- data/lib/shipeasy_admin/models/list_configs_response_data_inner.rb +351 -0
- data/lib/shipeasy_admin/models/list_configs_response_data_inner_drafts_value.rb +237 -0
- data/lib/shipeasy_admin/models/list_configs_response_data_inner_envs_value.rb +237 -0
- data/lib/shipeasy_admin/models/list_events_response_inner.rb +344 -0
- data/lib/shipeasy_admin/models/list_events_response_inner_properties_inner.rb +289 -0
- data/lib/shipeasy_admin/models/list_experiments_response.rb +178 -0
- data/lib/shipeasy_admin/models/list_experiments_response_data_inner.rb +688 -0
- data/lib/shipeasy_admin/models/list_experiments_response_data_inner_groups_inner.rb +258 -0
- data/lib/shipeasy_admin/models/list_gates_response.rb +178 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner.rb +395 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_enabled.rb +104 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_rules_inner.rb +238 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner.rb +105 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of.rb +375 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of1.rb +331 -0
- data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of_ramp.rb +319 -0
- data/lib/shipeasy_admin/models/list_i18n_drafts_response_inner.rb +272 -0
- data/lib/shipeasy_admin/models/list_i18n_keys_response.rb +195 -0
- data/lib/shipeasy_admin/models/list_i18n_keys_response_keys_inner.rb +284 -0
- data/lib/shipeasy_admin/models/list_i18n_profiles_response_inner.rb +224 -0
- data/lib/shipeasy_admin/models/list_killswitches_response.rb +178 -0
- data/lib/shipeasy_admin/models/list_killswitches_response_data_inner.rb +270 -0
- data/lib/shipeasy_admin/models/list_killswitches_response_data_inner_envs_value.rb +248 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner.rb +378 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir.rb +295 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg.rb +113 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of1.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of2.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of3.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of4.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of5.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of6.rb +188 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of7.rb +217 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of8.rb +233 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9.rb +240 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9_numerator.rb +266 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9_numerator_filters_inner.rb +264 -0
- data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_group_by.rb +228 -0
- data/lib/shipeasy_admin/models/list_ops_items_response_inner.rb +349 -0
- data/lib/shipeasy_admin/models/list_slack_channels_response.rb +195 -0
- data/lib/shipeasy_admin/models/list_slack_channels_response_channels_inner.rb +203 -0
- data/lib/shipeasy_admin/models/list_universes_response.rb +178 -0
- data/lib/shipeasy_admin/models/list_universes_response_data_inner.rb +305 -0
- data/lib/shipeasy_admin/models/notify_ops_request.rb +261 -0
- data/lib/shipeasy_admin/models/notify_ops_response.rb +193 -0
- data/lib/shipeasy_admin/models/publish_config_draft_request.rb +190 -0
- data/lib/shipeasy_admin/models/publish_config_draft_response.rb +260 -0
- data/lib/shipeasy_admin/models/publish_i18n_profile_request.rb +149 -0
- data/lib/shipeasy_admin/models/publish_i18n_profile_response.rb +404 -0
- data/lib/shipeasy_admin/models/push_i18n_keys_request.rb +205 -0
- data/lib/shipeasy_admin/models/push_i18n_keys_request_keys_inner.rb +192 -0
- data/lib/shipeasy_admin/models/push_i18n_keys_response.rb +261 -0
- data/lib/shipeasy_admin/models/reanalyze_experiment_response.rb +214 -0
- data/lib/shipeasy_admin/models/save_config_draft_request.rb +202 -0
- data/lib/shipeasy_admin/models/save_config_draft_response.rb +286 -0
- data/lib/shipeasy_admin/models/set_experiment_metrics_request.rb +168 -0
- data/lib/shipeasy_admin/models/set_experiment_metrics_request_metrics_inner.rb +216 -0
- data/lib/shipeasy_admin/models/set_experiment_metrics_response.rb +179 -0
- data/lib/shipeasy_admin/models/set_experiment_metrics_response_metrics_inner.rb +201 -0
- data/lib/shipeasy_admin/models/set_experiment_status_request.rb +190 -0
- data/lib/shipeasy_admin/models/set_experiment_status_response.rb +214 -0
- data/lib/shipeasy_admin/models/set_killswitch_switch_request.rb +264 -0
- data/lib/shipeasy_admin/models/set_killswitch_switch_response.rb +288 -0
- data/lib/shipeasy_admin/models/unset_killswitch_switch_request.rb +237 -0
- data/lib/shipeasy_admin/models/unset_killswitch_switch_response.rb +289 -0
- data/lib/shipeasy_admin/models/update_alert_rule_request.rb +306 -0
- data/lib/shipeasy_admin/models/update_alert_rule_response.rb +165 -0
- data/lib/shipeasy_admin/models/update_config_request.rb +198 -0
- data/lib/shipeasy_admin/models/update_config_response.rb +165 -0
- data/lib/shipeasy_admin/models/update_event_request.rb +198 -0
- data/lib/shipeasy_admin/models/update_event_response.rb +166 -0
- data/lib/shipeasy_admin/models/update_experiment_request.rb +746 -0
- data/lib/shipeasy_admin/models/update_experiment_response.rb +165 -0
- data/lib/shipeasy_admin/models/update_gate_request.rb +403 -0
- data/lib/shipeasy_admin/models/update_gate_response.rb +166 -0
- data/lib/shipeasy_admin/models/update_i18n_key_request.rb +176 -0
- data/lib/shipeasy_admin/models/update_i18n_key_response.rb +166 -0
- data/lib/shipeasy_admin/models/update_killswitch_request.rb +224 -0
- data/lib/shipeasy_admin/models/update_killswitch_response.rb +165 -0
- data/lib/shipeasy_admin/models/update_ops_item_request.rb +205 -0
- data/lib/shipeasy_admin/models/update_ops_item_response.rb +166 -0
- data/lib/shipeasy_admin/models/update_universe_request.rb +213 -0
- data/lib/shipeasy_admin/models/update_universe_response.rb +165 -0
- data/lib/shipeasy_admin/models/upsert_project_request.rb +222 -0
- data/lib/shipeasy_admin/models/upsert_project_response.rb +260 -0
- data/lib/shipeasy_admin/version.rb +15 -0
- data/lib/shipeasy_admin.rb +196 -0
- metadata +235 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Shipeasy
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate shipeasy:install`
|
|
8
|
+
#
|
|
9
|
+
# Scaffolds Shipeasy into a Rails app the Rails way:
|
|
10
|
+
#
|
|
11
|
+
# - writes config/initializers/shipeasy.rb (the single Shipeasy.configure call)
|
|
12
|
+
# - with --i18n, sets the public client key in that initializer AND injects
|
|
13
|
+
# `<%= i18n_head_tags %>` into the application layout's <head>
|
|
14
|
+
# - prints the keys / credentials next steps
|
|
15
|
+
#
|
|
16
|
+
# The gem's Railties already auto-mount the anon-id Rack middleware and the
|
|
17
|
+
# i18n view helpers, so this generator never touches middleware wiring — it
|
|
18
|
+
# only creates the things an app must own: the initializer, the layout tag,
|
|
19
|
+
# and your keys.
|
|
20
|
+
class InstallGenerator < Rails::Generators::Base
|
|
21
|
+
source_root File.expand_path("templates", __dir__)
|
|
22
|
+
|
|
23
|
+
desc "Create config/initializers/shipeasy.rb (and, with --i18n, wire the i18n view helpers)."
|
|
24
|
+
|
|
25
|
+
class_option :i18n, type: :boolean, default: false,
|
|
26
|
+
desc: "Also enable i18n: set the public client key and inject i18n_head_tags into the app layout"
|
|
27
|
+
class_option :poll, type: :boolean, default: true,
|
|
28
|
+
desc: "Use the background poll (long-running server). Pass --no-poll for a serverless one-shot fetch"
|
|
29
|
+
|
|
30
|
+
APP_LAYOUT = "app/views/layouts/application.html.erb"
|
|
31
|
+
|
|
32
|
+
def create_initializer
|
|
33
|
+
template "initializer.rb.tt", "config/initializers/shipeasy.rb"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inject_layout_helpers
|
|
37
|
+
return unless options[:i18n]
|
|
38
|
+
|
|
39
|
+
unless layout_exists?
|
|
40
|
+
say_status :skip, "#{APP_LAYOUT} not found — add <%= i18n_head_tags %> to your <head> by hand", :yellow
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if layout_already_wired?
|
|
45
|
+
say_status :identical, "#{APP_LAYOUT} already has i18n_head_tags", :blue
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Insert just before the closing </head>, picking up its indentation.
|
|
50
|
+
inject_into_file APP_LAYOUT, "<%= i18n_head_tags %>\n ", before: "</head>"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_next_steps
|
|
54
|
+
say ""
|
|
55
|
+
say " Shipeasy installed → config/initializers/shipeasy.rb", :green
|
|
56
|
+
say ""
|
|
57
|
+
say " Next steps:"
|
|
58
|
+
say " 1. Mint your keys: https://app.shipeasy.ai → Settings → SDK keys"
|
|
59
|
+
say " 2. Provide them (pick one):"
|
|
60
|
+
say " • ENV — set SHIPEASY_SERVER_KEY#{options[:i18n] ? " and SHIPEASY_CLIENT_KEY" : ""}"
|
|
61
|
+
say " • Rails credentials — bin/rails credentials:edit, then read"
|
|
62
|
+
say " them from Rails.application.credentials in the initializer"
|
|
63
|
+
if options[:i18n]
|
|
64
|
+
say " 3. i18n_head_tags is wired into your layout — run the Shipeasy"
|
|
65
|
+
say " i18n install to create your en:prod profile, then translate."
|
|
66
|
+
end
|
|
67
|
+
say ""
|
|
68
|
+
say " Read a flag anywhere per request:"
|
|
69
|
+
say " Shipeasy::Client.new(current_user).get_flag(\"new_checkout\")"
|
|
70
|
+
say ""
|
|
71
|
+
say " Docs: https://docs.shipeasy.ai", :cyan
|
|
72
|
+
say ""
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def layout_exists?
|
|
78
|
+
File.exist?(File.join(destination_root, APP_LAYOUT))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def layout_already_wired?
|
|
82
|
+
File.read(File.join(destination_root, APP_LAYOUT)).include?("i18n_head_tags")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shipeasy — feature flags, dynamic configs, kill switches, A/B experiments,
|
|
4
|
+
# metrics and (optional) i18n. Generated by `rails generate shipeasy:install`.
|
|
5
|
+
#
|
|
6
|
+
# Configure the SDK ONCE here at boot, then construct a cheap, user-bound
|
|
7
|
+
# `Shipeasy::Client.new(current_user)` per request — every read takes no user
|
|
8
|
+
# argument because the user is bound at construction:
|
|
9
|
+
#
|
|
10
|
+
# flags = Shipeasy::Client.new(current_user)
|
|
11
|
+
# flags.get_flag("new_checkout") # NO user arg — bound at construction
|
|
12
|
+
#
|
|
13
|
+
# Docs: https://docs.shipeasy.ai
|
|
14
|
+
Shipeasy.configure do |c|
|
|
15
|
+
# Required — your Shipeasy SERVER key. A server-side secret; never embed it in
|
|
16
|
+
# the browser. Read it from the environment, or from Rails credentials:
|
|
17
|
+
# c.api_key = Rails.application.credentials.dig(:shipeasy, :server_key)
|
|
18
|
+
c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY", nil)
|
|
19
|
+
|
|
20
|
+
<% if options[:poll] -%>
|
|
21
|
+
# Long-running server (Puma): keep flags fresh with the background poll
|
|
22
|
+
# (initial fetch + periodic refresh). Set to false for serverless /
|
|
23
|
+
# short-lived processes — they get a one-shot fetch and no background thread.
|
|
24
|
+
c.poll = true
|
|
25
|
+
<% else -%>
|
|
26
|
+
# Serverless / short-lived process: one-shot fetch, no background thread.
|
|
27
|
+
c.poll = false
|
|
28
|
+
<% end -%>
|
|
29
|
+
|
|
30
|
+
# Optional — map YOUR user object to the Shipeasy attribute hash that
|
|
31
|
+
# targeting evaluates against. Runs once, in the Shipeasy::Client constructor.
|
|
32
|
+
# Delete this line to pass the object through unchanged (identity default).
|
|
33
|
+
c.attributes = ->(user) { { "user_id" => user.id } }
|
|
34
|
+
<% if options[:i18n] -%>
|
|
35
|
+
|
|
36
|
+
# i18n view helpers (i18n_head_tags / i18n_t) — the PUBLIC client key. Safe to
|
|
37
|
+
# expose to the browser via the loader tag these helpers emit.
|
|
38
|
+
c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY", nil)
|
|
39
|
+
c.profile = "en:prod"
|
|
40
|
+
<% end -%>
|
|
41
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Optional Admin API client for the Shipeasy Ruby SDK.
|
|
4
|
+
#
|
|
5
|
+
# This subpackage is OFF by default: the main `shipeasy-sdk` entrypoint never
|
|
6
|
+
# requires it, and its HTTP dependency (`faraday`) is an optional development
|
|
7
|
+
# dependency — so `require "shipeasy-sdk"` never pulls it in. Require it
|
|
8
|
+
# explicitly when you want to *administer* resources (create gates, start
|
|
9
|
+
# experiments, …) from server code:
|
|
10
|
+
#
|
|
11
|
+
# require "faraday"
|
|
12
|
+
# require "shipeasy/admin"
|
|
13
|
+
#
|
|
14
|
+
# admin = Shipeasy::Admin::Client.new(
|
|
15
|
+
# api_key: ENV.fetch("SHIPEASY_ADMIN_KEY"),
|
|
16
|
+
# project_id: ENV.fetch("SHIPEASY_PROJECT_ID"),
|
|
17
|
+
# )
|
|
18
|
+
# admin.gates.list_gates
|
|
19
|
+
#
|
|
20
|
+
# Everything under `Shipeasy::Admin::Generated` (lib/shipeasy_admin*) is produced
|
|
21
|
+
# by scripts/gen_admin.sh from the OpenAPI spec and must not be edited by hand.
|
|
22
|
+
# `Client` is a thin auth/scoping wrapper; it does NOT add name->id resolution or
|
|
23
|
+
# percent->basis-point conversion (that facade lives in the Shipeasy CLI/MCP). The
|
|
24
|
+
# surface here is the raw, 1:1-with-the-spec REST API.
|
|
25
|
+
|
|
26
|
+
# Define the namespace BEFORE loading the generated client: its files use the
|
|
27
|
+
# compact `module Shipeasy::Admin::Generated` form, which requires Shipeasy::Admin
|
|
28
|
+
# to already exist (and `require "shipeasy/admin"` may run without the main SDK).
|
|
29
|
+
module Shipeasy
|
|
30
|
+
module Admin
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require "shipeasy_admin" # the generated client (loads faraday transitively)
|
|
35
|
+
|
|
36
|
+
module Shipeasy
|
|
37
|
+
module Admin
|
|
38
|
+
# Programmatic client for the Shipeasy Admin REST API. Each resource group is
|
|
39
|
+
# a lazily-constructed, memoized reader whose methods map 1:1 to the OpenAPI
|
|
40
|
+
# operations: #gates, #configs, #killswitches, #experiments, #universes,
|
|
41
|
+
# #metrics, #events, #alert_rules, #attributes, #projects, #ops, #i18n.
|
|
42
|
+
class Client
|
|
43
|
+
# Friendly reader name => generated Api class.
|
|
44
|
+
APIS = {
|
|
45
|
+
gates: Generated::GatesApi,
|
|
46
|
+
configs: Generated::ConfigsApi,
|
|
47
|
+
killswitches: Generated::KillswitchesApi,
|
|
48
|
+
experiments: Generated::ExperimentsApi,
|
|
49
|
+
universes: Generated::UniversesApi,
|
|
50
|
+
metrics: Generated::MetricsApi,
|
|
51
|
+
events: Generated::EventsApi,
|
|
52
|
+
alert_rules: Generated::AlertRulesApi,
|
|
53
|
+
attributes: Generated::AttributesApi,
|
|
54
|
+
projects: Generated::ProjectsApi,
|
|
55
|
+
ops: Generated::OpsApi,
|
|
56
|
+
i18n: Generated::I18nApi,
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# @param api_key [String] admin SDK key, sent as `Authorization: Bearer <api_key>`.
|
|
60
|
+
# @param project_id [String, nil] optional project id sent as the `X-Project-Id`
|
|
61
|
+
# header on every request. Operations also accept an explicit `x_project_id:`
|
|
62
|
+
# argument to override per call.
|
|
63
|
+
# @param base_url [String] API base URL. Defaults to https://shipeasy.ai
|
|
64
|
+
# (the spec's production server); use http://localhost:3000 for local dev.
|
|
65
|
+
def initialize(api_key:, project_id: nil, base_url: "https://shipeasy.ai")
|
|
66
|
+
config = Generated::Configuration.new
|
|
67
|
+
config.access_token = api_key
|
|
68
|
+
scheme, _, host = base_url.rpartition("://")
|
|
69
|
+
config.scheme = scheme unless scheme.empty?
|
|
70
|
+
config.host = host.empty? ? base_url : host
|
|
71
|
+
|
|
72
|
+
@api_client = Generated::ApiClient.new(config)
|
|
73
|
+
@api_client.default_headers["X-Project-Id"] = project_id if project_id
|
|
74
|
+
@apis = {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The underlying generated ApiClient (advanced/escape hatch).
|
|
78
|
+
attr_reader :api_client
|
|
79
|
+
|
|
80
|
+
APIS.each_key do |name|
|
|
81
|
+
define_method(name) { @apis[name] ||= APIS.fetch(name).new(@api_client) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/shipeasy/client.rb
CHANGED
|
@@ -55,6 +55,15 @@ module Shipeasy
|
|
|
55
55
|
def get_killswitch(name, switch_key = nil)
|
|
56
56
|
@engine.get_killswitch(name, switch_key)
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
def track(event_name, props = {})
|
|
60
|
+
id = @attributes["user_id"] || @attributes["anonymous_id"]
|
|
61
|
+
@engine.track(id, event_name, props)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log_exposure(experiment_name)
|
|
65
|
+
@engine.log_exposure(@attributes, experiment_name)
|
|
66
|
+
end
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
# Raised by Shipeasy::Client when constructed before Shipeasy.configure.
|
data/lib/shipeasy/config.rb
CHANGED
|
@@ -22,6 +22,27 @@ module Shipeasy
|
|
|
22
22
|
# ---- experimentation / SDK ----
|
|
23
23
|
attr_accessor :api_key, :base_url
|
|
24
24
|
|
|
25
|
+
# Advanced `configure` options — threaded into the global Engine `configure`
|
|
26
|
+
# builds, so callers never construct an Engine themselves:
|
|
27
|
+
# - env (default "prod"): deployment tag on see() events + usage telemetry.
|
|
28
|
+
# - disable_telemetry (default false): opt out of per-eval usage telemetry.
|
|
29
|
+
# - telemetry_url: override the telemetry endpoint (rarely needed).
|
|
30
|
+
# - private_attributes: attribute keys stripped from every outbound event
|
|
31
|
+
# before it leaves the process (they still drive targeting locally).
|
|
32
|
+
# - sticky_store: pin a user's experiment group across re-buckets.
|
|
33
|
+
attr_accessor :env, :disable_telemetry, :telemetry_url,
|
|
34
|
+
:private_attributes, :sticky_store
|
|
35
|
+
|
|
36
|
+
# Fetch lifecycle for the global engine `configure` builds:
|
|
37
|
+
# - init (default true): fire a one-shot fetch fire-and-forget so the first
|
|
38
|
+
# `Shipeasy::Client.new(user).get_flag(...)` resolves against real rules
|
|
39
|
+
# (ideal for serverless / short-lived processes).
|
|
40
|
+
# - poll (default false): start the background poll (initial fetch +
|
|
41
|
+
# periodic refresh) for a long-running server, so flags stay fresh
|
|
42
|
+
# without a redeploy. Configuration owns the lifecycle — you never call
|
|
43
|
+
# `engine.init` yourself.
|
|
44
|
+
attr_accessor :init, :poll
|
|
45
|
+
|
|
25
46
|
# Optional transform from YOUR user object (any shape) to the Shipeasy
|
|
26
47
|
# attribute hash every flag/experiment evaluation uses. A callable
|
|
27
48
|
# (lambda/proc or anything responding to #call). Default = identity (the
|
|
@@ -42,6 +63,13 @@ module Shipeasy
|
|
|
42
63
|
def initialize
|
|
43
64
|
@base_url = "https://edge.shipeasy.dev"
|
|
44
65
|
@attributes = nil
|
|
66
|
+
@init = true
|
|
67
|
+
@poll = false
|
|
68
|
+
@env = "prod"
|
|
69
|
+
@disable_telemetry = false
|
|
70
|
+
@telemetry_url = nil
|
|
71
|
+
@private_attributes = nil
|
|
72
|
+
@sticky_store = nil
|
|
45
73
|
|
|
46
74
|
@profile = "default"
|
|
47
75
|
@default_chunk = "index"
|
|
@@ -105,23 +133,180 @@ module Shipeasy
|
|
|
105
133
|
@engine
|
|
106
134
|
end
|
|
107
135
|
|
|
108
|
-
# Build + register the one global engine (first-config-wins).
|
|
109
|
-
# one-shot
|
|
136
|
+
# Build + register the one global engine (first-config-wins). Kicks off the
|
|
137
|
+
# configured fetch lifecycle (one-shot by default; the background poll when
|
|
138
|
+
# `c.poll = true`) fire-and-forget. Idempotent within a process.
|
|
110
139
|
def register_engine!(cfg)
|
|
111
140
|
return @engine if @engine && @engine_pid == Process.pid
|
|
112
141
|
@engine_pid = Process.pid
|
|
113
|
-
engine = Engine.new(
|
|
142
|
+
engine = Engine.new(
|
|
143
|
+
api_key: cfg.api_key,
|
|
144
|
+
base_url: cfg.base_url,
|
|
145
|
+
env: cfg.env,
|
|
146
|
+
disable_telemetry: cfg.disable_telemetry,
|
|
147
|
+
telemetry_url: cfg.telemetry_url,
|
|
148
|
+
private_attributes: cfg.private_attributes,
|
|
149
|
+
sticky_store: cfg.sticky_store,
|
|
150
|
+
)
|
|
114
151
|
@engine = engine
|
|
115
152
|
# Capture +engine+ in the closure (not the @engine ivar, which a concurrent
|
|
116
153
|
# reset/reconfigure could nil out before the thread runs).
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
if cfg.poll
|
|
155
|
+
Thread.new do
|
|
156
|
+
engine.init # initial fetch + background poll thread
|
|
157
|
+
rescue => e
|
|
158
|
+
warn "[shipeasy] configure(poll) background poll failed: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
elsif cfg.init
|
|
161
|
+
Thread.new do
|
|
162
|
+
engine.init_once
|
|
163
|
+
rescue => e
|
|
164
|
+
warn "[shipeasy] configure() one-shot fetch failed: #{e.message}"
|
|
165
|
+
end
|
|
121
166
|
end
|
|
122
167
|
engine
|
|
123
168
|
end
|
|
124
169
|
|
|
170
|
+
# ---- configure() test/offline siblings -----------------------------------
|
|
171
|
+
#
|
|
172
|
+
# Drop-in siblings of `Shipeasy.configure` for tests and offline evaluation.
|
|
173
|
+
# Unlike `configure` (first-config-wins), these REPLACE the registered global
|
|
174
|
+
# engine, so a suite can reconfigure between cases. After either, you read the
|
|
175
|
+
# same way: `Shipeasy::Client.new(user)`.
|
|
176
|
+
|
|
177
|
+
# Configure Shipeasy in TEST MODE — no api key, zero network, ever. Seed the
|
|
178
|
+
# values your code under test should see via the override args, then read
|
|
179
|
+
# through the ordinary `Shipeasy::Client.new(user)`:
|
|
180
|
+
#
|
|
181
|
+
# Shipeasy.configure_for_testing(flags: { "new_checkout" => true })
|
|
182
|
+
# Shipeasy::Client.new({ "user_id" => "u_1" }).get_flag("new_checkout") # => true
|
|
183
|
+
#
|
|
184
|
+
# flags: { name => bool } forced get_flag results
|
|
185
|
+
# configs: { name => value } forced get_config results
|
|
186
|
+
# experiments: { name => [group, params] } forced enrolments
|
|
187
|
+
# attributes: same transform as configure (default identity)
|
|
188
|
+
def configure_for_testing(flags: nil, configs: nil, experiments: nil, attributes: nil)
|
|
189
|
+
engine = Engine.for_testing
|
|
190
|
+
apply_overrides(engine, flags, configs, experiments)
|
|
191
|
+
install_global_engine(engine, attributes)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Configure Shipeasy OFFLINE — evaluate the REAL rules from an in-memory
|
|
195
|
+
# snapshot or a JSON file, with no network. Provide exactly one source:
|
|
196
|
+
#
|
|
197
|
+
# snapshot: { "flags" => <body of /sdk/flags>, "experiments" => <body of /sdk/experiments> }
|
|
198
|
+
# path: "snapshot.json" (a JSON file of the same shape)
|
|
199
|
+
#
|
|
200
|
+
# Optional flags/configs/experiments overrides layer on top (same shapes as
|
|
201
|
+
# configure_for_testing). Replaces any previously-configured engine.
|
|
202
|
+
def configure_for_offline(snapshot: nil, path: nil, flags: nil, configs: nil, experiments: nil, attributes: nil)
|
|
203
|
+
engine =
|
|
204
|
+
if path
|
|
205
|
+
Engine.from_file(path)
|
|
206
|
+
elsif snapshot
|
|
207
|
+
s = snapshot.transform_keys(&:to_s)
|
|
208
|
+
Engine.from_snapshot(flags: s["flags"], experiments: s["experiments"])
|
|
209
|
+
else
|
|
210
|
+
raise Error, "Shipeasy.configure_for_offline requires snapshot: or path:"
|
|
211
|
+
end
|
|
212
|
+
apply_overrides(engine, flags, configs, experiments)
|
|
213
|
+
install_global_engine(engine, attributes)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# ---- package-level helpers (so callers never name the Engine) -------------
|
|
217
|
+
|
|
218
|
+
# On-the-spot overrides layered on top of whatever configure_for_testing /
|
|
219
|
+
# configure_for_offline (or a live configure) set up — they win over the blob
|
|
220
|
+
# until clear_overrides. Require a prior configure* call.
|
|
221
|
+
def override_flag(name, value)
|
|
222
|
+
require_engine("override_flag").override_flag(name, value)
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def override_config(name, value)
|
|
227
|
+
require_engine("override_config").override_config(name, value)
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def override_experiment(name, group, params)
|
|
232
|
+
require_engine("override_experiment").override_experiment(name, group, params)
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Drop EVERY override — including the seed from configure_for_testing (test
|
|
237
|
+
# mode has no blob beneath); under configure_for_offline it reverts to the
|
|
238
|
+
# snapshot.
|
|
239
|
+
def clear_overrides
|
|
240
|
+
require_engine("clear_overrides").clear_overrides
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Register a poll listener fired after a background poll fetches NEW data
|
|
245
|
+
# (HTTP 200, not 304). Requires configure(poll: true). Returns an unsubscribe
|
|
246
|
+
# proc. Accepts a block or any callable.
|
|
247
|
+
def on_change(callable = nil, &block)
|
|
248
|
+
require_engine("on_change").on_change(callable, &block)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# SSR tag helpers — delegate to the configured global engine, so you never
|
|
252
|
+
# touch it. i18n_script_tag carries the PUBLIC client key (not the server
|
|
253
|
+
# key); bootstrap_script_tag embeds no key.
|
|
254
|
+
def i18n_script_tag(client_key, profile: "en:prod", base_url: nil)
|
|
255
|
+
require_engine("i18n_script_tag").i18n_script_tag(client_key, profile: profile, base_url: base_url)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def bootstrap_script_tag(user, anon_id: nil, i18n_profile: "en:prod", base_url: nil)
|
|
259
|
+
require_engine("bootstrap_script_tag").bootstrap_script_tag(
|
|
260
|
+
user, anon_id: anon_id, i18n_profile: i18n_profile, base_url: base_url
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# see() structured error reporting — package-level, dispatched through the
|
|
265
|
+
# last-constructed default client (the engine configure built). Never raises
|
|
266
|
+
# into caller code; a call before any client exists warns and no-ops.
|
|
267
|
+
def see(problem)
|
|
268
|
+
Shipeasy::SDK.see(problem)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def see_violation(name)
|
|
272
|
+
Shipeasy::SDK.see_violation(name)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def control_flow_exception(err)
|
|
276
|
+
Shipeasy::SDK.control_flow_exception(err)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Replace the registered global engine + attributes transform (used by the
|
|
280
|
+
# configure_for_* siblings — unlike configure, they replace so a test suite
|
|
281
|
+
# can reconfigure between cases). Returns the engine.
|
|
282
|
+
def install_global_engine(engine, attributes)
|
|
283
|
+
config.attributes = attributes
|
|
284
|
+
@engine = engine
|
|
285
|
+
@engine_pid = Process.pid
|
|
286
|
+
engine
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Apply the configure_for_* override args onto an engine.
|
|
290
|
+
def apply_overrides(engine, flags, configs, experiments)
|
|
291
|
+
(flags || {}).each { |name, value| engine.override_flag(name, value) }
|
|
292
|
+
(configs || {}).each { |name, value| engine.override_config(name, value) }
|
|
293
|
+
(experiments || {}).each do |name, spec|
|
|
294
|
+
group, params = spec # spec is [group, params]
|
|
295
|
+
engine.override_experiment(name, group, params)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The global engine, or raise a helpful error naming the package-level fn the
|
|
300
|
+
# caller used before any configure*.
|
|
301
|
+
def require_engine(fn_name)
|
|
302
|
+
e = engine
|
|
303
|
+
return e unless e.nil?
|
|
304
|
+
|
|
305
|
+
raise Error, "Shipeasy.#{fn_name} called before Shipeasy.configure " \
|
|
306
|
+
"{ |c| c.api_key = … } (or configure_for_testing / " \
|
|
307
|
+
"configure_for_offline). Call one once at app boot."
|
|
308
|
+
end
|
|
309
|
+
|
|
125
310
|
# Reset the config back to defaults — primarily for tests.
|
|
126
311
|
def reset_config!
|
|
127
312
|
@config = nil
|
data/lib/shipeasy/engine.rb
CHANGED
|
@@ -90,15 +90,19 @@ module Shipeasy
|
|
|
90
90
|
|
|
91
91
|
# Build a no-network, immediately-usable client for tests. Telemetry is
|
|
92
92
|
# disabled, init/init_once/track are no-ops (never fetch), and no api_key
|
|
93
|
-
# is required.
|
|
93
|
+
# is required. The client is immediately READY against an empty blob (so a
|
|
94
|
+
# missing gate resolves FLAG_NOT_FOUND, not CLIENT_NOT_READY — parity with
|
|
95
|
+
# the other SDKs). Seed it with override_flag / override_config /
|
|
94
96
|
# override_experiment, then call the normal getters.
|
|
95
97
|
def self.for_testing(env: "prod")
|
|
96
|
-
new(
|
|
98
|
+
client = new(
|
|
97
99
|
api_key: "test",
|
|
98
100
|
env: env,
|
|
99
101
|
disable_telemetry: true,
|
|
100
102
|
test_mode: true,
|
|
101
103
|
)
|
|
104
|
+
client.send(:load_snapshot, {}, {})
|
|
105
|
+
client
|
|
102
106
|
end
|
|
103
107
|
|
|
104
108
|
# Build an offline client from a JSON snapshot file. The file holds the
|
|
@@ -300,17 +304,21 @@ module Shipeasy
|
|
|
300
304
|
|
|
301
305
|
# Read a killswitch from the cached flags blob. Without +switch_key+,
|
|
302
306
|
# returns true when the whole killswitch is killed. With +switch_key+,
|
|
303
|
-
# returns true when that specific per-key switch is on
|
|
304
|
-
#
|
|
307
|
+
# returns true when that specific named per-key switch is on — and when
|
|
308
|
+
# the key isn't configured on the killswitch, FALLS BACK to the top-level
|
|
309
|
+
# value (so an unconfigured key behaves exactly like the no-key call).
|
|
310
|
+
# Unknown killswitches return false. Not user-scoped.
|
|
305
311
|
def get_killswitch(name, switch_key = nil)
|
|
306
312
|
@telemetry.emit("ks", name)
|
|
307
313
|
ks = @mutex.synchronize { @flags_blob&.dig("killswitches", name.to_s) }
|
|
308
314
|
return false unless ks
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Eval.enabled?(
|
|
315
|
+
unless switch_key.nil?
|
|
316
|
+
switches = ks["switches"] || {}
|
|
317
|
+
key = switch_key.to_s
|
|
318
|
+
return Eval.enabled?(switches[key]) if switches.key?(key)
|
|
319
|
+
# key not configured → fall through to the top-level value
|
|
313
320
|
end
|
|
321
|
+
Eval.enabled?(ks["killed"])
|
|
314
322
|
end
|
|
315
323
|
|
|
316
324
|
# Batch-evaluate every loaded gate, config and experiment for +user+ into
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
# require "open_feature/sdk"
|
|
12
12
|
# require "shipeasy/sdk/openfeature"
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# client.init
|
|
14
|
+
# Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY"); c.poll = true }
|
|
16
15
|
#
|
|
17
16
|
# OpenFeature::SDK.configure do |config|
|
|
18
|
-
# config.set_provider(Shipeasy::OpenFeature::Provider.new
|
|
17
|
+
# config.set_provider(Shipeasy::OpenFeature::Provider.new) # uses the configured global
|
|
19
18
|
# end
|
|
20
19
|
#
|
|
21
20
|
# of = OpenFeature::SDK.build_client
|
|
@@ -38,6 +37,8 @@ rescue LoadError => e
|
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
require_relative "../engine"
|
|
40
|
+
require_relative "../config"
|
|
41
|
+
require_relative "../client"
|
|
41
42
|
|
|
42
43
|
module Shipeasy
|
|
43
44
|
module OpenFeature
|
|
@@ -66,7 +67,17 @@ module Shipeasy
|
|
|
66
67
|
|
|
67
68
|
attr_reader :metadata
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
# Construct the provider. With no argument it resolves the global engine
|
|
71
|
+
# configured via `Shipeasy.configure(...)`, so callers never build an
|
|
72
|
+
# Engine themselves — construct it AFTER your `Shipeasy.configure` call.
|
|
73
|
+
# Pass an explicit engine only for advanced/multi-key setups.
|
|
74
|
+
def initialize(client = nil)
|
|
75
|
+
client ||= Shipeasy.engine
|
|
76
|
+
if client.nil?
|
|
77
|
+
raise Shipeasy::Error, "Shipeasy::OpenFeature::Provider.new needs " \
|
|
78
|
+
"Shipeasy.configure { |c| c.api_key = … } to have run first " \
|
|
79
|
+
"(or pass an explicit engine)."
|
|
80
|
+
end
|
|
70
81
|
@client = client
|
|
71
82
|
@metadata = OF::ProviderMetadata.new(name: "shipeasy").freeze
|
|
72
83
|
end
|
|
@@ -123,12 +134,28 @@ module Shipeasy
|
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
# OpenFeature `track()` → Shipeasy `track()`. No-ops without a targeting key.
|
|
126
|
-
def track(tracking_event_name, evaluation_context: nil,
|
|
137
|
+
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
|
|
127
138
|
ctx = normalize_context(evaluation_context)
|
|
128
139
|
user_id = ctx["targeting_key"] || ctx["user_id"]
|
|
129
140
|
return if user_id.nil? || user_id.to_s.empty?
|
|
130
141
|
|
|
131
|
-
props =
|
|
142
|
+
# Base props = the evaluation-context attributes (minus the identity
|
|
143
|
+
# keys), with the tracking-event details merged on top.
|
|
144
|
+
props = ctx.reject { |k, _| k == "targeting_key" || k == "user_id" }
|
|
145
|
+
|
|
146
|
+
detail_fields = if tracking_event_details.respond_to?(:fields)
|
|
147
|
+
tracking_event_details.fields.transform_keys(&:to_s)
|
|
148
|
+
elsif tracking_event_details.is_a?(Hash)
|
|
149
|
+
tracking_event_details.transform_keys(&:to_s)
|
|
150
|
+
else
|
|
151
|
+
{}
|
|
152
|
+
end
|
|
153
|
+
props = props.merge(detail_fields)
|
|
154
|
+
|
|
155
|
+
if tracking_event_details.respond_to?(:value) && !tracking_event_details.value.nil?
|
|
156
|
+
props["value"] = tracking_event_details.value
|
|
157
|
+
end
|
|
158
|
+
|
|
132
159
|
@client.track(user_id, tracking_event_name, props)
|
|
133
160
|
end
|
|
134
161
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Shipeasy
|
|
6
|
+
module SDK
|
|
7
|
+
# `shipeasy-skill` — install the bundled Shipeasy agent skill into a project.
|
|
8
|
+
#
|
|
9
|
+
# RubyGems has no safe post-install hook (gems don't run code on install;
|
|
10
|
+
# installers run non-interactively), so installing the skill is an explicit,
|
|
11
|
+
# opt-in command:
|
|
12
|
+
#
|
|
13
|
+
# shipeasy-skill install # → .claude/skills/shipeasy-ruby/SKILL.md
|
|
14
|
+
# shipeasy-skill install --dir path/ # custom destination (file or dir)
|
|
15
|
+
# shipeasy-skill install --force # overwrite an existing file
|
|
16
|
+
# shipeasy-skill print # write the skill to stdout
|
|
17
|
+
#
|
|
18
|
+
# The skill (`docs/skill/SKILL.md`) is shipped inside the gem, so this reads
|
|
19
|
+
# it with no network — relative to this file, which works both from an
|
|
20
|
+
# installed gem and a source checkout.
|
|
21
|
+
module Skill
|
|
22
|
+
DEFAULT_DEST = ".claude/skills/shipeasy-ruby/SKILL.md"
|
|
23
|
+
|
|
24
|
+
# The bundled SKILL.md, read from docs/skill/SKILL.md (a sibling of lib/ in
|
|
25
|
+
# both the installed gem and a source checkout).
|
|
26
|
+
def self.skill_text
|
|
27
|
+
path = File.expand_path("../../../docs/skill/SKILL.md", __dir__)
|
|
28
|
+
File.read(path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Copy the skill to +dest+ (a file, or a directory it's written into).
|
|
32
|
+
def self.install(dest, force: false)
|
|
33
|
+
dest = File.join(dest, "SKILL.md") if File.directory?(dest) || File.extname(dest).empty?
|
|
34
|
+
if File.exist?(dest) && !force
|
|
35
|
+
warn "shipeasy-skill: refusing to overwrite #{dest} — pass --force"
|
|
36
|
+
return 1
|
|
37
|
+
end
|
|
38
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
39
|
+
File.write(dest, skill_text)
|
|
40
|
+
puts "shipeasy-skill: installed the Shipeasy agent skill → #{dest}"
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.main(argv)
|
|
45
|
+
cmd = argv.shift
|
|
46
|
+
case cmd
|
|
47
|
+
when "install"
|
|
48
|
+
dest = DEFAULT_DEST
|
|
49
|
+
force = false
|
|
50
|
+
while (arg = argv.shift)
|
|
51
|
+
case arg
|
|
52
|
+
when "--dir" then dest = argv.shift
|
|
53
|
+
when "--force" then force = true
|
|
54
|
+
else
|
|
55
|
+
warn "shipeasy-skill: unknown argument #{arg}"
|
|
56
|
+
return 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
install(dest, force: force)
|
|
60
|
+
when "print"
|
|
61
|
+
puts skill_text
|
|
62
|
+
0
|
|
63
|
+
else
|
|
64
|
+
puts <<~USAGE
|
|
65
|
+
shipeasy-skill — install the Shipeasy Ruby agent skill into your project.
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
shipeasy-skill install [--dir PATH] [--force] copy SKILL.md (default: #{DEFAULT_DEST})
|
|
69
|
+
shipeasy-skill print print the skill to stdout
|
|
70
|
+
USAGE
|
|
71
|
+
0
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/shipeasy/sdk/version.rb
CHANGED