betterplace-bi 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.semaphore/semaphore.yml +26 -0
- data/.tool-versions +2 -0
- data/.utilsrc +26 -0
- data/Gemfile +5 -0
- data/README.md +76 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/betterplace-bi.gemspec +39 -0
- data/lib/betterplace-bi.rb +1 -0
- data/lib/bi/ab_test_helper.rb +81 -0
- data/lib/bi/api.rb +115 -0
- data/lib/bi/commands/base.rb +11 -0
- data/lib/bi/commands/collector.rb +32 -0
- data/lib/bi/commands/connection.rb +25 -0
- data/lib/bi/commands/delete.rb +33 -0
- data/lib/bi/commands/serializer.rb +40 -0
- data/lib/bi/commands/update.rb +29 -0
- data/lib/bi/commands.rb +10 -0
- data/lib/bi/commands_job.rb +24 -0
- data/lib/bi/event.rb +52 -0
- data/lib/bi/planning_value_parser.rb +100 -0
- data/lib/bi/planning_value_validations.rb +15 -0
- data/lib/bi/railtie.rb +13 -0
- data/lib/bi/request_analyzer.rb +64 -0
- data/lib/bi/session_id.rb +11 -0
- data/lib/bi/shared_value.rb +102 -0
- data/lib/bi/tracking.rb +28 -0
- data/lib/bi/type_generator.rb +79 -0
- data/lib/bi/update_error.rb +4 -0
- data/lib/bi/updater.rb +61 -0
- data/lib/bi/version.rb +8 -0
- data/lib/bi.rb +27 -0
- data/lib/tasks/bime.rake +55 -0
- data/spec/bi/ab_test_helper_spec.rb +145 -0
- data/spec/bi/commands/collector_spec.rb +26 -0
- data/spec/bi/commands/connection_spec.rb +15 -0
- data/spec/bi/commands_job_spec.rb +24 -0
- data/spec/bi/commands_spec.rb +46 -0
- data/spec/bi/event_spec.rb +77 -0
- data/spec/bi/planning_value_parser_spec.rb +94 -0
- data/spec/bi/request_analyzer_spec.rb +75 -0
- data/spec/bi/shared_value_spec.rb +47 -0
- data/spec/bi/tracking_spec.rb +37 -0
- data/spec/bi/type_generator_spec.rb +44 -0
- data/spec/bi/updater_spec.rb +61 -0
- data/spec/bime_dir/.keep +0 -0
- data/spec/config/bi.yml +18 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/models.rb +106 -0
- metadata +331 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 119422d8d08ceabf4379d677d556c4dc2b6bcb32f0ae4f7b6304164bc7fabd59
|
4
|
+
data.tar.gz: 2cccf0803769aec1947716a60ea2d1dec45074785347b6a28058a92c99f7d3e9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 34c3d5aa9e9f363011bdf77fa53e5b4bfaaac9728a29f916d14e0b1b7769e0364c03e8c46ee1572e50316cfe09fe5e9ff18e9375fe7fb9de3078a3f20fee2373
|
7
|
+
data.tar.gz: 3b96febf3b0a6aa22ef134980e4a574603ea8d2020661af16abac9016a6354c84dc9d65a216664cfcc576e8fb790caf68feeb8e58a79e0af4cd5d31cde68430a
|
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
version: v1.0
|
2
|
+
name: Betterplace-BI pipeline
|
3
|
+
agent:
|
4
|
+
machine:
|
5
|
+
type: e1-standard-2
|
6
|
+
os_image: ubuntu2004
|
7
|
+
|
8
|
+
blocks:
|
9
|
+
- name: "Unit Tests"
|
10
|
+
task:
|
11
|
+
env_vars:
|
12
|
+
# Matches the configuration used in sem-service
|
13
|
+
- name: RAILS_ENV
|
14
|
+
value: test
|
15
|
+
jobs:
|
16
|
+
- name: RSpec
|
17
|
+
commands:
|
18
|
+
- checkout
|
19
|
+
|
20
|
+
# Setup ruby
|
21
|
+
- sem-version ruby $(awk '/^ruby/ { print $2 }' .tool-versions)
|
22
|
+
|
23
|
+
# Setup gems
|
24
|
+
- bundle config set path 'vendor/bundle'
|
25
|
+
- bundle install
|
26
|
+
- bundle exec rake spec
|
data/.tool-versions
ADDED
data/.utilsrc
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# vim: set ft=ruby:
|
2
|
+
|
3
|
+
search do
|
4
|
+
prune_dirs /\A(\.svn|\.git|CVS|tmp|tags|coverage|pkg)\z/
|
5
|
+
skip_files /(\A\.|\.sw[pon]\z|\.(log|fnm|jpg|jpeg|png|pdf|svg)\z|tags|~\z)/i
|
6
|
+
end
|
7
|
+
|
8
|
+
discover do
|
9
|
+
prune_dirs /\A(\.svn|\.git|CVS|tmp|tags|coverage|pkg)\z/
|
10
|
+
skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
|
11
|
+
binary false
|
12
|
+
end
|
13
|
+
|
14
|
+
strip_spaces do
|
15
|
+
prune_dirs /\A(\..*|CVS|pkg)\z/
|
16
|
+
skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
|
17
|
+
end
|
18
|
+
|
19
|
+
probe do
|
20
|
+
test_framework :rspec
|
21
|
+
#include_dirs 'features'
|
22
|
+
end
|
23
|
+
|
24
|
+
ssh_tunnel do
|
25
|
+
terminal_multiplexer :tmux
|
26
|
+
end
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Betterplace-BI
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
This library transfers BI data from an application to a BIME-server.
|
6
|
+
|
7
|
+
## Configuration
|
8
|
+
|
9
|
+
Via a `config/bi.yml` file in YAML like so:
|
10
|
+
|
11
|
+
```
|
12
|
+
---
|
13
|
+
production:
|
14
|
+
update: yes
|
15
|
+
auth:
|
16
|
+
username: bi
|
17
|
+
password: secret
|
18
|
+
clear: http://localhost:1234/api/v1/all/:id
|
19
|
+
endpoints:
|
20
|
+
BI::TestModelValue:
|
21
|
+
POST: http://localhost:1234/api/v1/tests
|
22
|
+
DELETE: http://localhost:1234/api/v1/test/:id
|
23
|
+
bime_dir:
|
24
|
+
<%= Pathname.pwd.join('../bime') %>
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
For every Rails model `TestModel` define a class named
|
30
|
+
`BI::TestModelValue` like this:
|
31
|
+
|
32
|
+
```
|
33
|
+
class BI::TestModelValue
|
34
|
+
include BI::SharedValue
|
35
|
+
|
36
|
+
def self.update_class
|
37
|
+
::TestModel
|
38
|
+
end
|
39
|
+
|
40
|
+
field type: 'string', db: 'gorm:"type:uuid;primary_key"'
|
41
|
+
def id
|
42
|
+
@object.id
|
43
|
+
end
|
44
|
+
|
45
|
+
field type: '*int64', db: 'gorm:"type:bigint"'
|
46
|
+
def number
|
47
|
+
10 ** 10
|
48
|
+
end
|
49
|
+
|
50
|
+
field type: '*time.Time', db: bp_timestamp # ISO timestamp string, e. g. "2018-11-05T15:40:14.362219+01:00"
|
51
|
+
def created_at
|
52
|
+
iso_timestamp(@object.created_at)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Compute the values to be transfered by accessing @object and define GO types
|
58
|
+
and GORM struct tags for these values.
|
59
|
+
|
60
|
+
Also include `BI::Updater` in `TestModel` like so:
|
61
|
+
|
62
|
+
```
|
63
|
+
class TestModel < ApplicationRecord
|
64
|
+
include BI::Updater
|
65
|
+
|
66
|
+
has_one :dependent
|
67
|
+
|
68
|
+
def trigger_dependent_updates
|
69
|
+
dependent.schedule_bi_update_job(true)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
This software is licensed under the Apache 2.0 license.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# vim: set filetype=ruby et sw=2 ts=2:
|
2
|
+
|
3
|
+
require 'gem_hadar'
|
4
|
+
|
5
|
+
GemHadar do
|
6
|
+
name 'betterplace-bi'
|
7
|
+
path_name 'bi'
|
8
|
+
path_module 'BI'
|
9
|
+
author 'Florian Frank'
|
10
|
+
email 'flori@ping.de'
|
11
|
+
homepage "https://github.com/betterplace/#{name}"
|
12
|
+
summary 'Business Intelligence library'
|
13
|
+
description 'This library contains functionality to send/store BI and AB-testing data'
|
14
|
+
test_dir 'spec'
|
15
|
+
ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', 'coverage', '.rvmrc',
|
16
|
+
'.AppleDouble', '.DS_Store', '.byebug_history', 'errors.lst', 'tags'
|
17
|
+
|
18
|
+
readme 'README.md'
|
19
|
+
title "#{name.camelize} -- BI library"
|
20
|
+
licenses << 'Apache-2.0'
|
21
|
+
|
22
|
+
dependency 'json'
|
23
|
+
dependency 'tins'
|
24
|
+
dependency 'complex_config'
|
25
|
+
dependency 'globalid'
|
26
|
+
dependency 'excon'
|
27
|
+
dependency 'rails'
|
28
|
+
dependency 'sinatra'
|
29
|
+
dependency 'betterlog'
|
30
|
+
dependency 'mize'
|
31
|
+
development_dependency 'rake'
|
32
|
+
development_dependency 'simplecov'
|
33
|
+
development_dependency 'rspec'
|
34
|
+
development_dependency 'byebug'
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.7.0
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# stub: betterplace-bi 0.7.0 ruby lib
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "betterplace-bi".freeze
|
6
|
+
s.version = "0.7.0".freeze
|
7
|
+
|
8
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
9
|
+
s.require_paths = ["lib".freeze]
|
10
|
+
s.authors = ["Florian Frank".freeze]
|
11
|
+
s.date = "2024-04-08"
|
12
|
+
s.description = "This library contains functionality to send/store BI and AB-testing data".freeze
|
13
|
+
s.email = "flori@ping.de".freeze
|
14
|
+
s.extra_rdoc_files = ["README.md".freeze, "lib/betterplace-bi.rb".freeze, "lib/bi.rb".freeze, "lib/bi/ab_test_helper.rb".freeze, "lib/bi/api.rb".freeze, "lib/bi/commands.rb".freeze, "lib/bi/commands/base.rb".freeze, "lib/bi/commands/collector.rb".freeze, "lib/bi/commands/connection.rb".freeze, "lib/bi/commands/delete.rb".freeze, "lib/bi/commands/serializer.rb".freeze, "lib/bi/commands/update.rb".freeze, "lib/bi/commands_job.rb".freeze, "lib/bi/event.rb".freeze, "lib/bi/planning_value_parser.rb".freeze, "lib/bi/planning_value_validations.rb".freeze, "lib/bi/railtie.rb".freeze, "lib/bi/request_analyzer.rb".freeze, "lib/bi/session_id.rb".freeze, "lib/bi/shared_value.rb".freeze, "lib/bi/tracking.rb".freeze, "lib/bi/type_generator.rb".freeze, "lib/bi/update_error.rb".freeze, "lib/bi/updater.rb".freeze, "lib/bi/version.rb".freeze]
|
15
|
+
s.files = [".gitignore".freeze, ".semaphore/semaphore.yml".freeze, ".tool-versions".freeze, ".utilsrc".freeze, "Gemfile".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "betterplace-bi.gemspec".freeze, "lib/betterplace-bi.rb".freeze, "lib/bi.rb".freeze, "lib/bi/ab_test_helper.rb".freeze, "lib/bi/api.rb".freeze, "lib/bi/commands.rb".freeze, "lib/bi/commands/base.rb".freeze, "lib/bi/commands/collector.rb".freeze, "lib/bi/commands/connection.rb".freeze, "lib/bi/commands/delete.rb".freeze, "lib/bi/commands/serializer.rb".freeze, "lib/bi/commands/update.rb".freeze, "lib/bi/commands_job.rb".freeze, "lib/bi/event.rb".freeze, "lib/bi/planning_value_parser.rb".freeze, "lib/bi/planning_value_validations.rb".freeze, "lib/bi/railtie.rb".freeze, "lib/bi/request_analyzer.rb".freeze, "lib/bi/session_id.rb".freeze, "lib/bi/shared_value.rb".freeze, "lib/bi/tracking.rb".freeze, "lib/bi/type_generator.rb".freeze, "lib/bi/update_error.rb".freeze, "lib/bi/updater.rb".freeze, "lib/bi/version.rb".freeze, "lib/tasks/bime.rake".freeze, "spec/bi/ab_test_helper_spec.rb".freeze, "spec/bi/commands/collector_spec.rb".freeze, "spec/bi/commands/connection_spec.rb".freeze, "spec/bi/commands_job_spec.rb".freeze, "spec/bi/commands_spec.rb".freeze, "spec/bi/event_spec.rb".freeze, "spec/bi/planning_value_parser_spec.rb".freeze, "spec/bi/request_analyzer_spec.rb".freeze, "spec/bi/shared_value_spec.rb".freeze, "spec/bi/tracking_spec.rb".freeze, "spec/bi/type_generator_spec.rb".freeze, "spec/bi/updater_spec.rb".freeze, "spec/bime_dir/.keep".freeze, "spec/config/bi.yml".freeze, "spec/spec_helper.rb".freeze, "spec/support/models.rb".freeze]
|
16
|
+
s.homepage = "https://github.com/betterplace/betterplace-bi".freeze
|
17
|
+
s.licenses = ["Apache-2.0".freeze]
|
18
|
+
s.rdoc_options = ["--title".freeze, "Betterplace-bi -- BI library".freeze, "--main".freeze, "README.md".freeze]
|
19
|
+
s.rubygems_version = "3.4.21".freeze
|
20
|
+
s.summary = "Business Intelligence library".freeze
|
21
|
+
s.test_files = ["spec/bi/ab_test_helper_spec.rb".freeze, "spec/bi/commands/collector_spec.rb".freeze, "spec/bi/commands/connection_spec.rb".freeze, "spec/bi/commands_job_spec.rb".freeze, "spec/bi/commands_spec.rb".freeze, "spec/bi/event_spec.rb".freeze, "spec/bi/planning_value_parser_spec.rb".freeze, "spec/bi/request_analyzer_spec.rb".freeze, "spec/bi/shared_value_spec.rb".freeze, "spec/bi/tracking_spec.rb".freeze, "spec/bi/type_generator_spec.rb".freeze, "spec/bi/updater_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/support/models.rb".freeze]
|
22
|
+
|
23
|
+
s.specification_version = 4
|
24
|
+
|
25
|
+
s.add_development_dependency(%q<gem_hadar>.freeze, ["~> 1.11.0".freeze])
|
26
|
+
s.add_development_dependency(%q<rake>.freeze, [">= 0".freeze])
|
27
|
+
s.add_development_dependency(%q<simplecov>.freeze, [">= 0".freeze])
|
28
|
+
s.add_development_dependency(%q<rspec>.freeze, [">= 0".freeze])
|
29
|
+
s.add_development_dependency(%q<byebug>.freeze, [">= 0".freeze])
|
30
|
+
s.add_runtime_dependency(%q<json>.freeze, [">= 0".freeze])
|
31
|
+
s.add_runtime_dependency(%q<tins>.freeze, [">= 0".freeze])
|
32
|
+
s.add_runtime_dependency(%q<complex_config>.freeze, [">= 0".freeze])
|
33
|
+
s.add_runtime_dependency(%q<globalid>.freeze, [">= 0".freeze])
|
34
|
+
s.add_runtime_dependency(%q<excon>.freeze, [">= 0".freeze])
|
35
|
+
s.add_runtime_dependency(%q<rails>.freeze, [">= 0".freeze])
|
36
|
+
s.add_runtime_dependency(%q<sinatra>.freeze, [">= 0".freeze])
|
37
|
+
s.add_runtime_dependency(%q<betterlog>.freeze, [">= 0".freeze])
|
38
|
+
s.add_runtime_dependency(%q<mize>.freeze, [">= 0".freeze])
|
39
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'bi'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module BI
|
2
|
+
module ABTestHelper
|
3
|
+
OVERRIDE_PARAM_NAME = "ab_test"
|
4
|
+
|
5
|
+
# Parameter +name+ is the name of the AB-test, the first element of
|
6
|
+
# +alternatives+ is the control. +request+ is a ActionDispatch::Request
|
7
|
+
# object.
|
8
|
+
def ab_test(name, *alternatives, request: self.request, &block)
|
9
|
+
alternatives.flatten!
|
10
|
+
alternatives.empty? and return
|
11
|
+
session_id, mode = nil, nil
|
12
|
+
chosen =
|
13
|
+
case
|
14
|
+
when override = ab_test_override?(name, *alternatives, request: request)
|
15
|
+
mode = :override
|
16
|
+
override
|
17
|
+
when ab_test_ignore_user?(request: request)
|
18
|
+
mode = :ignore
|
19
|
+
alternatives.first
|
20
|
+
when session = request.ask_and_send(:session)
|
21
|
+
session.loaded? or session.send(:load!)
|
22
|
+
session_id = BI::SessionID.session_id(session)
|
23
|
+
n = if session_id.empty?
|
24
|
+
mode = :random
|
25
|
+
rand(1 << 128)
|
26
|
+
else
|
27
|
+
mode = :session
|
28
|
+
Digest::MD5.hexdigest([ name, session_id ] * ?:).to_i(16)
|
29
|
+
end
|
30
|
+
alternatives[n % alternatives.size]
|
31
|
+
else
|
32
|
+
mode = :default
|
33
|
+
n = rand(1 << 128)
|
34
|
+
alternatives[n % alternatives.size]
|
35
|
+
end
|
36
|
+
Log.info(
|
37
|
+
"session.id=#{session_id} => #{chosen} was chosen for #{name} (#{mode})",
|
38
|
+
meta: { module: 'bi' }
|
39
|
+
)
|
40
|
+
block&.(chosen) || chosen
|
41
|
+
end
|
42
|
+
|
43
|
+
def ab_test_ignore_user?(request: self.request)
|
44
|
+
request and BI::RequestAnalyzer.new(request).ignore?
|
45
|
+
end
|
46
|
+
|
47
|
+
# It's possible to pick the control with the URL parameter
|
48
|
+
# 'ab_test=control' or ab_test=0, the first alternative by
|
49
|
+
# ab_test=alternative or ab_test=1, further alternatives by 2, 3,… It's
|
50
|
+
# also possible to pick one by using the version's name like ab_test=foo.
|
51
|
+
# If there is more than one AB-Test running, this parameter can be
|
52
|
+
# ab_test[name]=… to only switch the AB-Test
|
53
|
+
# named +name+ to the control or alternatives. This way it's possible to
|
54
|
+
# view all the possible permutations of controls and alternatives if you're
|
55
|
+
# crazy enough.
|
56
|
+
def ab_test_override?(name, *alternatives, request: self.request)
|
57
|
+
alternatives.flatten!
|
58
|
+
overriden = request.params[OVERRIDE_PARAM_NAME] rescue nil
|
59
|
+
case
|
60
|
+
when overriden.respond_to?(:to_str)
|
61
|
+
ab_test_override_alternative(overriden.to_str, *alternatives)
|
62
|
+
when overriden.respond_to?(:to_hash)
|
63
|
+
ab_test_override_alternative(overriden.to_hash[name], *alternatives)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def ab_test_override_alternative(override, *alternatives)
|
68
|
+
alternatives.flatten!
|
69
|
+
case override
|
70
|
+
when 'control'
|
71
|
+
alternatives.first
|
72
|
+
when 'alternative'
|
73
|
+
alternatives.second
|
74
|
+
when /\A\d+\z/
|
75
|
+
alternatives[$&.to_i] || alternatives.first
|
76
|
+
when *alternatives
|
77
|
+
override
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/bi/api.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module BI
|
4
|
+
module API
|
5
|
+
|
6
|
+
module Helper
|
7
|
+
include BI::ABTestHelper
|
8
|
+
|
9
|
+
def self.included(modul)
|
10
|
+
modul.extend self
|
11
|
+
end
|
12
|
+
|
13
|
+
def hostname
|
14
|
+
@hostname ||= Socket.gethostname rescue 'n/a'
|
15
|
+
end
|
16
|
+
|
17
|
+
def status_code(symbol)
|
18
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol]
|
19
|
+
end
|
20
|
+
|
21
|
+
def rails_request
|
22
|
+
ActionDispatch::Request.new(env)
|
23
|
+
end
|
24
|
+
|
25
|
+
def rails_session
|
26
|
+
rails_request.session
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_result(result, code: :ok, headers: {})
|
30
|
+
headers = headers.merge(
|
31
|
+
'Content-Type' => 'application/json',
|
32
|
+
)
|
33
|
+
code = status_code(code)
|
34
|
+
[ code, headers, result ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_not_found
|
38
|
+
result = JSON(
|
39
|
+
hostname: hostname,
|
40
|
+
message: 'not found',
|
41
|
+
)
|
42
|
+
render_result(result, code: :not_found)
|
43
|
+
end
|
44
|
+
|
45
|
+
def render_exception(exception, code: :internal_server_error)
|
46
|
+
result = JSON(
|
47
|
+
hostname: hostname,
|
48
|
+
error: exception.class.name,
|
49
|
+
message: exception.message,
|
50
|
+
)
|
51
|
+
render_result(result, code: code)
|
52
|
+
end
|
53
|
+
|
54
|
+
def ab_test_result(channel, request)
|
55
|
+
config = cc.ab_tests[channel.to_s] or return
|
56
|
+
analyzer = BI::RequestAnalyzer.new(request)
|
57
|
+
current = ab_test(config.name, config.alternatives, request: rails_request)
|
58
|
+
version = "#{config.name}/#{current}"
|
59
|
+
{
|
60
|
+
channel: channel,
|
61
|
+
version: version,
|
62
|
+
session: BI::SessionID.session_id(request.session),
|
63
|
+
device_type: analyzer.device_type,
|
64
|
+
user_agent: analyzer.user_agent,
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class App < Sinatra::Base
|
70
|
+
include BI::API::Helper
|
71
|
+
|
72
|
+
before do
|
73
|
+
rr = rails_request
|
74
|
+
Log.info "Received #{rr.method} to #{rr.path}",
|
75
|
+
type: 'bi_request',
|
76
|
+
meta: {
|
77
|
+
module: 'bi',
|
78
|
+
url: rr.url,
|
79
|
+
method: rr.method,
|
80
|
+
params: rr.params,
|
81
|
+
referer: rr.referer,
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
get '/' do
|
86
|
+
render_result JSON(
|
87
|
+
endpoints: {
|
88
|
+
'/bi' => { methods: [ 'GET' ] },
|
89
|
+
'/bi/ab_test/:channel' => { methods: [ 'GET', 'POST' ] },
|
90
|
+
}
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
get '/ab_test/:channel' do
|
95
|
+
channel = params.fetch('channel')
|
96
|
+
result = ab_test_result(channel, rails_request) or return
|
97
|
+
render_result JSON(result)
|
98
|
+
end
|
99
|
+
|
100
|
+
post '/ab_test/:channel' do
|
101
|
+
channel = params.fetch('channel')
|
102
|
+
result = ab_test_result(channel, rails_request) or render_not_found
|
103
|
+
event_data = JSON(
|
104
|
+
request.body.tap(&:rewind).read
|
105
|
+
).symbolize_keys_recursive
|
106
|
+
event_data.update result
|
107
|
+
BI::Event.new.write(**event_data)
|
108
|
+
render_result JSON(result)
|
109
|
+
rescue => e
|
110
|
+
Log.error e, meta: { module: 'bi' }
|
111
|
+
render_exception(e, code: :unprocessable_entity)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module BI
|
2
|
+
module Commands
|
3
|
+
class Collector
|
4
|
+
def initialize
|
5
|
+
@first_object = nil
|
6
|
+
@commands = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(object, value_class)
|
10
|
+
@first_object ||= object
|
11
|
+
if object&.destroyed?
|
12
|
+
if url = value_class.url(method: 'DELETE', id: object.id)
|
13
|
+
@commands << BI::Commands::Delete.new(url: url)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
value = value_class.new(object)
|
17
|
+
@commands << BI::Commands::Update.new(value: value)
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def schedule_job?(object)
|
23
|
+
@first_object == object
|
24
|
+
end
|
25
|
+
|
26
|
+
def schedule_job
|
27
|
+
BI::CommandsJob.new(*@commands).enqueue
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module BI
|
2
|
+
module Commands
|
3
|
+
module Connection
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def create_connection
|
7
|
+
Log.info("Connecting BI Server at %s now" % cc.bi.server)
|
8
|
+
Excon.new(
|
9
|
+
cc.bi.server,
|
10
|
+
user: cc.bi.auth.username,
|
11
|
+
password: cc.bi.auth.password,
|
12
|
+
persistent: true,
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect_to_server
|
17
|
+
connection = create_connection
|
18
|
+
yield connection
|
19
|
+
self
|
20
|
+
ensure
|
21
|
+
connection&.reset
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bi/commands/base'
|
2
|
+
|
3
|
+
module BI
|
4
|
+
module Commands
|
5
|
+
class Delete < BI::Commands::Base
|
6
|
+
|
7
|
+
def url
|
8
|
+
data[:url]
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform_via(connection)
|
12
|
+
Log.info("Sending BI Delete to #{url}")
|
13
|
+
response = connection.delete(
|
14
|
+
path: url,
|
15
|
+
headers: {
|
16
|
+
'Content-Type' => 'application/json',
|
17
|
+
'Accept' => 'application/json',
|
18
|
+
}
|
19
|
+
)
|
20
|
+
case response.status
|
21
|
+
when 0...300
|
22
|
+
Log.info "Successfully sent delete to #{url}."
|
23
|
+
when 404
|
24
|
+
Log.warn "Duplicate deletion sent to #{url}: status=#{response.status}\n#{response.body}"
|
25
|
+
# Already deleted
|
26
|
+
else
|
27
|
+
Log.warn "Failed to sent delete to #{url}: status=#{response.status}\n#{response.body}"
|
28
|
+
raise BI::UpdateError, "status=#{response.status}: #{response.body}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module BI
|
2
|
+
module Commands
|
3
|
+
class Serializer < ActiveJob::Serializers::ObjectSerializer
|
4
|
+
# Checks if an argument should be serialized by this serializer.
|
5
|
+
def serialize?(argument)
|
6
|
+
argument.is_a? ::BI::Commands::Base
|
7
|
+
end
|
8
|
+
|
9
|
+
GLOBALID_KEY = "_bi_globalid"
|
10
|
+
|
11
|
+
# Converts an object to a simpler representative using supported object
|
12
|
+
# types. The recommended representative is a Hash with a specific key. Keys
|
13
|
+
# can be of basic types only. You should call `super` to add the custom
|
14
|
+
# serializer type to the hash.
|
15
|
+
def serialize(command)
|
16
|
+
hash = { class: command.class.name }
|
17
|
+
hash |= command.data.to_h.transform_values { |v|
|
18
|
+
if v.is_a?(GlobalID::Identification)
|
19
|
+
v = { GLOBALID_KEY => v.to_global_id.to_s }
|
20
|
+
end
|
21
|
+
v
|
22
|
+
}
|
23
|
+
hash.stringify_keys!
|
24
|
+
super(hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Converts serialized value into a proper object.
|
28
|
+
def deserialize(hash)
|
29
|
+
hash.delete("class").constantize.new(
|
30
|
+
hash.transform_values { |v|
|
31
|
+
if h = v.ask_and_send(:to_hash) and h.include?(GLOBALID_KEY)
|
32
|
+
v = GlobalID::Locator.locate h[GLOBALID_KEY]
|
33
|
+
end
|
34
|
+
v
|
35
|
+
}.symbolize_keys
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'bi/commands/base'
|
2
|
+
|
3
|
+
module BI
|
4
|
+
module Commands
|
5
|
+
class Update < BI::Commands::Base
|
6
|
+
def value
|
7
|
+
data[:value]
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform_via(connection)
|
11
|
+
Log.info("Sending BI Update value for %s" % value)
|
12
|
+
response = connection.post(
|
13
|
+
path: value.url,
|
14
|
+
headers: {
|
15
|
+
'Content-Type' => 'application/json',
|
16
|
+
'Accept' => 'application/json',
|
17
|
+
},
|
18
|
+
body: value.to_json,
|
19
|
+
)
|
20
|
+
if response.status < 300
|
21
|
+
Log.info "Successfully sent update for #{value}."
|
22
|
+
else
|
23
|
+
Log.warn "Failed to sent update for #{value}: status=#{response.status}\n#{response.body}"
|
24
|
+
raise BI::UpdateError, "status=#{response.status}: #{response.body}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/bi/commands.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_job/arguments'
|
2
|
+
|
3
|
+
module BI
|
4
|
+
class CommandsJob < ::ActiveJob::Base
|
5
|
+
include BI::Commands::Connection
|
6
|
+
|
7
|
+
queue_as :bime
|
8
|
+
|
9
|
+
retry_on Excon::Error::Socket, # handle temporary dns resolution problems
|
10
|
+
Excon::Error::Timeout # handle timeouts
|
11
|
+
|
12
|
+
retry_on(ActiveJob::DeserializationError, wait: :polynomially_longer, attempts: 4) {}
|
13
|
+
|
14
|
+
def perform(*commands)
|
15
|
+
commands.empty? and return
|
16
|
+
connect_to_server do |connection|
|
17
|
+
commands.each do |command|
|
18
|
+
command.perform_via(connection)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|