Skip to content

Commit fa42fc0

Browse files
authored
Merge pull request #760 from splitrb/dashboard-modernization
Dashboard modernization
2 parents 290741d + 437a5f0 commit fa42fc0

26 files changed

Lines changed: 1009 additions & 679 deletions

.eslintignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

.eslintrc

Lines changed: 0 additions & 213 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pkg/*
55
*.rbc
66
.idea
77
coverage
8+
node_modules
89
issues.rtf
910
dump.rdb
1011
.gitignore

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ If you have an experiment called `button_color` with alternatives called `red` a
168168

169169
will always have red buttons. This won't be stored in your session or count towards to results, unless you set the `store_override` configuration option.
170170

171+
The dashboard's "Force for current user" button does the same thing for your browser, by setting a `split_override` cookie. Because an override is not counted, an experiment you have pinned can keep reading zero participants. The dashboard lists any overrides active in your browser at the top of the page, and lets you clear them individually or all at once.
172+
171173
In the event you want to disable all tests without having to know the individual experiment names, add a `SPLIT_DISABLE` query parameter.
172174

173175
http://myawesomesite.com?SPLIT_DISABLE=true

eslint.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const js = require("@eslint/js");
2+
const globals = require("globals");
3+
4+
module.exports = [
5+
{ ignores: ["coverage/"] },
6+
js.configs.recommended,
7+
{
8+
files: ["lib/**/*.js"],
9+
languageOptions: {
10+
globals: globals.browser
11+
},
12+
rules: {
13+
"no-empty": ["error", { allowEmptyCatch: true }]
14+
}
15+
},
16+
{
17+
files: ["eslint.config.js"],
18+
languageOptions: {
19+
globals: globals.node
20+
}
21+
}
22+
];

lib/split.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ module Split
3030
extend self
3131
attr_accessor :configuration
3232

33+
OVERRIDE_COOKIE_NAME = "split_override"
34+
3335
# Accepts:
3436
# 1. A redis URL (valid for `Redis.new(url: url)`)
3537
# 2. an options hash compatible with `Redis.new`

lib/split/dashboard.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "sinatra/base"
44
require "split"
55
require "bigdecimal"
6+
require "split/dashboard/experiment"
67
require "split/dashboard/helpers"
78
require "split/dashboard/pagination_helpers"
89

@@ -44,9 +45,21 @@ class Dashboard < Sinatra::Base
4445
experiment = Split::ExperimentCatalog.find(params[:experiment])
4546
alternative = Split::Alternative.new(params[:alternative], experiment.name)
4647

47-
cookies = JSON.parse(request.cookies["split_override"]) rescue {}
48-
cookies[experiment.name] = alternative.name
49-
response.set_cookie("split_override", { value: cookies.to_json, path: "/" })
48+
write_overrides(active_overrides.merge(experiment.name => alternative.name))
49+
50+
redirect url("/")
51+
end
52+
53+
post "/clear_override" do
54+
overrides = active_overrides
55+
overrides.delete(params[:experiment])
56+
write_overrides(overrides)
57+
58+
redirect url("/")
59+
end
60+
61+
post "/clear_overrides" do
62+
response.delete_cookie(Split::OVERRIDE_COOKIE_NAME, path: "/")
5063

5164
redirect url("/")
5265
end

lib/split/dashboard/experiment.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require "forwardable"
4+
5+
module Split
6+
class DashboardExperiment
7+
def initialize(experiment, goal = nil)
8+
@experiment = experiment
9+
@goal = goal
10+
end
11+
12+
def alternatives
13+
@alternatives ||= @experiment.alternatives.map do |alternative|
14+
DashboardAlternative.new(alternative, @experiment, @goal)
15+
end
16+
end
17+
18+
def extra_columns
19+
@extra_columns ||= alternatives.flat_map { |alternative| alternative.extra_info.keys }.uniq
20+
end
21+
22+
def extra_totals
23+
@extra_totals ||= extra_columns.to_h do |column|
24+
values = alternatives.map { |alternative| alternative.extra_info[column] }.compact
25+
[column, values.any? && values.all?(Numeric) ? values.sum : "N/A"]
26+
end
27+
end
28+
29+
def total_participants
30+
alternatives.sum(&:participants)
31+
end
32+
33+
def total_unfinished
34+
alternatives.sum(&:unfinished)
35+
end
36+
37+
def total_completed
38+
alternatives.sum(&:completed)
39+
end
40+
end
41+
42+
class DashboardAlternative
43+
extend Forwardable
44+
def_delegators :@alternative, :name, :control?
45+
46+
attr_reader :participants, :unfinished, :completed, :extra_info
47+
48+
def initialize(alternative, experiment, goal = nil)
49+
@alternative = alternative
50+
@experiment = experiment
51+
@goal = goal
52+
53+
@participants = alternative.participant_count
54+
@unfinished = alternative.unfinished_count
55+
@completed = alternative.completed_count(goal)
56+
@extra_info = alternative.extra_info
57+
end
58+
59+
def conversion_rate
60+
@conversion_rate ||= @alternative.conversion_rate(@goal)
61+
end
62+
63+
def conversion_delta
64+
return if control?
65+
66+
control_rate = @experiment.control.conversion_rate(@goal)
67+
return if control_rate <= 0 || conversion_rate == control_rate
68+
69+
conversion_rate / control_rate - 1
70+
end
71+
72+
def z_score
73+
@z_score ||= @alternative.z_score(@goal)
74+
end
75+
76+
def p_winner
77+
@p_winner ||= @alternative.p_winner(@goal)
78+
end
79+
80+
def winner?
81+
@experiment.winner.name == name
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)