Skip to content
Browse files

API endpoint for listing all split tests and getting split tests deta…

…il with historical data.

Split test API documentation moved from code to README.
  • Loading branch information...
1 parent 5f7c534 commit b5677696efd7141483be8899966fe38dfa81eac0 @assaf committed Apr 14, 2012
View
102 server/README.md
@@ -163,58 +163,98 @@ DELETE /v1/activity/:id
```
-### Participate In Split Test
+## Web API for Split Tests
+
+### List Tests
+
+```
+PUT /v1/split
+```
+
+Returns a list of all active split test.
+
+Response JSON document includes a single property `tests` with an array of split
+tests, each an object with the following properties:
+* `id` - Test identifier
+* `title` - Human readable title
+* `created` - Timestamp when test was created
+
+### Get Test Results
+
+```
+PUT /v1/split/:test
+```
+
+Returns information about a split test.
+
+Response json document includes the following properties:
+* `id` - Test identifier
+* `title` - Human readable title
+* `created` - Timestamp when test was created
+* `alternatives` - Array of alternatives
+
+Each alternative includes the following properties:
+* `title` - Title of this alternative
+* `participants` - Number of participants in this test
+* `completed` - Number of participants that completed this test
+* `data` - Time series data
+
+Time series data is an array of entries, one for each hour, consisting of:
+* `time` - The time
+* `participants` - Number of participants in this test
+* `completed` - Number of participants that completed this test
+
+If the test does not exist, returns status code 404.
+
+### Add Participant
```
-PUT /v1/split/:test/:participant
+POST /v1/split/:test/:participant
```
-Request path specifies the test and participant identifiers.
+Send this request to indicate participant joined the test.
-The body is a JSON document (form parameters also supported) with the following
-properties:
+Request path specifies the test and participant identifier.
-* alternative - Alternative number (0 ... n)
-* outcome - A numeric value
+The request document must specify a single parameter (in any supported media
+type):
+* `alternative` - The alternative chosen for this participant, either 0 (A) or 1
+ (B)
-To indicate that participant is taking part in this split test, send a request
-with alternative number. You can send this request any number of times, only
-the first update is stored.
+Returns status code 200 and a JSON document with one property:
+* `alternative` - The alternative decided for this participant
-To indicate that participant converted, send a request with alternative number
-abd the outcome. You can send this request any number of times, only the first
-update is stored.
+If the test identifier or alternative are invalid, the request returns status
+code 400.
-If successful, this request returns status code 200 and a JSON document with the
-following properties:
+### Record Completion
-* participant - Participant identifeir
-* alternative - Alternative number
-* outcome - Recorded outcome
+```
+POST /v1/split/:test/:participant/completed
+```
-If participant was already added with a different alternative, the request
-returns status code 409 (Conflict) and the above JSON document.
+Send this request to indicate participant completed the test (converted).
-If the test identifier, alternative number or outcome are invalid, the request
-returns status code 400.
+Request path specifies the test and participant identifier.
+Returns status code 204 (No content).
-### Retrieve Participation In Split Test
+### Retrieve Participant
```
GET /v1/split/:test/:participant
```
-Request path specifies the test and participant identifiers.
+Returns information about participant.
-If successful, this request returns status code 200 and a JSON document with the
-following properties:
+Request path specifies the test and participant identifier.
-* participant - Participant identifeir
-* joined - When participant joined this test (RFC3339)
-* alternative - Alternative number
-* completed - When participant completed this test (RFC3339)
-* outcome - Recorded outcome
+Response JSON document includes the following properties:
+* `participant` - Identifier
+* `joined` - Timestamp when participant joined experiment
+* `alternative` - Alternative number
+* `completed` - Timestamp when participant completed experiment
+* `outcome` - Recorded outcome
If the participant never joined this split test, the request returns status code
404.
View
102 server/models/split_test.coffee
@@ -206,9 +206,9 @@ SplitTest =
id: ids[i]
title: test.title
created: Date.create(test.created)
- alternatives: [a,b].map(({ participants, completed })->
- participants: parseInt(participants)
- completed: parseInt(completed)
+ alternatives: [a,b].map(({ title, participants, completed }, i)->
+ participants: parseInt(participants) || 0
+ completed: parseInt(completed) || 0
title: title || "AB"[i]
)
)
@@ -227,72 +227,42 @@ SplitTest =
multi.hgetall "#{base_key}"
multi.hgetall "#{base_key}.0"
multi.hgetall "#{base_key}.1"
- multi.exec (error, [test, a, b])->
+ multi.hgetall "#{base_key}.participants"
+ multi.zrange "#{base_key}.joined", 0, -1, "withscores"
+ multi.hgetall "#{base_key}.converted.0"
+ multi.hgetall "#{base_key}.converted.1"
+ multi.exec (error, [test, a, b, participants, joined, converted_a, converted_b])->
+ converted = [converted_a, converted_b]
return callback(error) if error
- if test?.title
- callback null,
- id: test_id
- title: test.title
- created: Date.create(test.created)
- alternatives: [a,b].map(({ participants, completed, title }, i)->
- participants: parseInt(participants)
- completed: parseInt(completed)
- title: title || "AB"[i]
- )
- else
+ unless test?.title # no such test
callback(null)
+ return
-
- # Loads test data and passes callback array with one element for each
- # alternative, containing:
- # title - Alternative title
- # weight - Designated weight
- # data - Data points
- #
- # Each data point has the properties:
- # time - Time stamp (in hour increments) RFC3999
- # participants - How many participants joined during that hour
- # completed - How many of these participants completed the test
- data: (test_id, callback)->
- base_key = SplitTest.baseKey(test_id)
- Async.waterfall [
- (done)->
- # First we need to determine which participant is assigned what
- # alternative. This gives us a map from participant ID to alternative
- # number.
- redis.hgetall "#{base_key}.participants", done
-
- , (participants, doneJoined)->
- # Next we need to determine how many participants joined each
- # alternative in any given hour.
- hourly = [{}, {}]
- redis.zrange "#{base_key}.joined", 0, -1, "withscores", (error, joined)->
- return doneJoined(error) if error
- for [id, time] in joined.inGroupsOf(2)
- time -= time % 3600000 # round down to nearest hour
- set = hourly[participants[id]]
- hour = set[time] ||= { time: Date.create(time).toISOString() }
- hour.participants = (hour.participants || 0) + 1
- doneJoined(null, hourly)
-
- , (hourly, doneConverted)->
- # Load everything we know about conversion for each given time slot, and
- # update the data record.
- Async.map [0, 1], (alternative, doneEach)->
- set = hourly[alternative]
- redis.hgetall "#{base_key}.converted.#{alternative}", (error, converted)->
- return doneEach(error) if error
- for _, entry of set
- entry.converted = parseInt(converted[entry.time]) || 0
- doneEach(null, set)
- , doneConverted
-
- , (hourly, done)->
- # Now let's turn each hourly map into a sorted array.
- sorted = (Object.values(set).sort("time") for set in hourly)
- done(null, sorted)
-
- ], callback
+ # Next we need to determine how many participants joined each
+ # alternative in any given hour.
+ hourly = [{}, {}]
+ for [id, time] in joined.inGroupsOf(2)
+ time -= time % 3600000 # round down to nearest hour
+ set = hourly[participants[id]]
+ hour = set[time] ||= { time: Date.create(time).toISOString() }
+ hour.participants = (hour.participants || 0) + 1
+
+ # And from that we can determine how many converted in each hour.
+ [0, 1].each (alternative)->
+ set = hourly[alternative]
+ for _, entry of set
+ entry.converted = parseInt(converted[alternative][entry.time]) || 0
+
+ callback null,
+ id: test_id
+ title: test.title
+ created: Date.create(test.created)
+ alternatives: [a,b].map(({ participants, completed, title }, alternative)->
+ participants: parseInt(participants) || 0
+ completed: parseInt(completed) || 0
+ title: title || "AB"[alternative]
+ data: Object.values(hourly[alternative]).sort("time")
+ )
# -- Utility --
View
62 server/routes/api_split_tests.coffee
@@ -4,12 +4,15 @@ server = require("../config/server")
SplitTest = require("../models/split_test")
+# Returns a list of all active split test.
+server.get "/v1/split", (req, res, next)->
+ SplitTest.list (error, tests)->
+ if tests
+ res.send tests: tests
+ else
+ next(error)
+
# Returns information about a split test.
-#
-# Response JSON document includes the following properties:
-# id - Test identifier
-# title - Human readable title
-# created - Timestamp when test was created
server.get "/v1/split/:test", (req, res, next)->
SplitTest.load req.params.test, (error, test)->
if test
@@ -21,15 +24,7 @@ server.get "/v1/split/:test", (req, res, next)->
else
next(error)
-
# Send this request to indicate participant joined the test.
-#
-# The request must specify a single parameter (in any supported media type):
-# alternative - The alternative chosen for this participant, either
-# 0 (A) or 1 (B)
-#
-# Returns status code 200 and a JSON document with one property:
-# alternative - The alternative decided for this participant
server.post "/v1/split/:test/:participant", (req, res, next)->
alternative = parseInt(req.body.alternative, 10)
try
@@ -41,10 +36,7 @@ server.post "/v1/split/:test/:participant", (req, res, next)->
catch error
res.send error.message, 400
-
# Send this request to indicate participant completed the test.
-#
-# Returns status code 204 (No content).
server.post "/v1/split/:test/:participant/completed", (req, res, next)->
try
SplitTest.completed req.params.test, req.params.participant, (error)->
@@ -55,39 +47,11 @@ server.post "/v1/split/:test/:participant/completed", (req, res, next)->
catch error
res.send error.message, 404
-
-# Returns the raw data part of this split test.
-#
-# Response JSON document is an array with one element for each alternative.
-# Each element is itself an array with the properties:
-# time - Date/time at 1 hour resoultion (RFC3339)
-# participants - Number of participants joined during that hour
-# completed - How many of these participants completed the test
-server.get "/v1/split/:test/data", (req, res, next)->
- SplitTest.data req.params.test, (error, data)->
- if data
- res.send data
- else
- next(error)
-
-
# Returns information about participant.
-#
-# Path includes test and participant identifier.
-#
-# Response JSON document includes the following properties:
-# participant - Identifier
-# joined - Timestamp when participant joined experiment
-# alternative - Alternative number
-# completed - Timestamp when participant completed experiment
-# outcome - Recorded outcome
server.get "/v1/split/:test/:participant", (req, res, next)->
- try
- SplitTest.getParticipant req.params.test, req.params.participant, (error, result)->
- if result
- res.send result
- else
- next(error)
- catch error
- res.send error.message, 404
+ SplitTest.getParticipant req.params.test, req.params.participant, (error, result)->
+ if result
+ res.send result
+ else
+ next(error)
View
150 server/test/api_split_test.coffee
@@ -13,16 +13,72 @@ describe "API split test", ->
base_url = "http://localhost:3003/v1/split/"
before Helper.once
+ before Helper.setup
+ before (done)->
+ vanity = new Vanity(host: "localhost:3003")
+ split = vanity.split("foo-bar")
+ # Record participants
+ Async.forEach ["8c0521ee", "c2659ef8", "be8bb5b1", "f3cb65e5", "6d9d70c5"],
+ (id, done)->
+ split.show id, done
+ # Records completion
+ , ->
+ Async.forEach ["8c0521ee", "f3cb65e5"],
+ (id, done)->
+ split.completed id, done
+ , done
+
+
+ # -- List tests --
+
+ describe "list tests", ->
+
+ statusCode = null
+ tests = null
+
+ before (done)->
+ # Collect the results
+ request.get base_url, (error, response)->
+ { statusCode, body } = response
+ tests = JSON.parse(response.body).tests if statusCode == 200
+ done()
+
+ it "should return 200", ->
+ assert.equal statusCode, 200
+
+ it "should return array of tests", ->
+ assert.equal tests.length, 1
+
+ it "should return identifier for each test", ->
+ assert.equal tests[0].id, "foo-bar"
+
+ it "should return title for each test", ->
+ assert.equal tests[0].title, "Foo Bar"
+
+ it "should return created time for each test", ->
+ assert Date.create(tests[0].created) - Date.now() < 1000
+
+ it "should return title for each alternative", ->
+ assert.equal tests[0].alternatives[0].title, "A"
+ assert.equal tests[0].alternatives[1].title, "B"
+
+ it "should return participants for each alternative", ->
+ assert.equal tests[0].alternatives[0].participants, 2
+ assert.equal tests[0].alternatives[1].participants, 3
+
+ it "should return completed for each alternative", ->
+ assert.equal tests[0].alternatives[0].completed, 0
+ assert.equal tests[0].alternatives[1].completed, 2
# -- Retrieving test --
-
+
describe "get test", ->
- statusCode = body = null
+ statusCode = null
+ test = null
describe "no such test", ->
- before Helper.setup
before (done)->
request.get base_url + "nosuch", (_, response)->
{ statusCode, body } = response
@@ -31,83 +87,49 @@ describe "API split test", ->
it "should return 404", ->
assert.equal statusCode, 404
+
+ describe "existing test", ->
- describe "some participants", ->
-
- test_url = base_url + "foobar1"
- test = null
-
- before Helper.setup
- before (done)->
- vanity = new Vanity(host: "localhost:3003")
- split = vanity.split("foobar1")
- # Record participants
- Async.forEach ["8c0521ee", "c2659ef8", "be8bb5b1", "f3cb65e5", "6d9d70c5"],
- (id, done)->
- split.show id, done
- , done
before (done)->
# Collect the results
- request.get test_url, (error, response)->
+ request.get base_url + "foo-bar", (error, response)->
{ statusCode, body } = response
test = JSON.parse(response.body) if statusCode == 200
done()
it "should return 200", ->
assert.equal statusCode, 200
- it "should return created time", ->
- assert Date.create(test.created) - Date.now() < 1000
+ it "should return test identifier", ->
+ assert.equal test.id, "foo-bar"
- it "should return alternatives", ->
- [a, b] = test.alternatives
- assert a && b
- assert a.participants
- assert.equal a.title, "A"
- assert.equal b.title, "B"
+ it "should return test title", ->
+ assert.equal test.title, "Foo Bar"
+ it "should return test created time", ->
+ assert Date.create(test.created) - Date.now() < 1000
- # -- Test data --
+ it "should return title for each alternative", ->
+ assert.equal test.alternatives[0].title, "A"
+ assert.equal test.alternatives[1].title, "B"
- describe "data", ->
+ it "should return participants for each alternative", ->
+ assert.equal test.alternatives[0].participants, 2
+ assert.equal test.alternatives[1].participants, 3
- test_url = base_url + "foobar2"
- split = null
- result = statusCode = null
+ it "should return completed for each alternative", ->
+ assert.equal test.alternatives[0].completed, 0
+ assert.equal test.alternatives[1].completed, 2
- before Helper.setup
- before (done)->
- vanity = new Vanity(host: "localhost:3003")
- split = vanity.split("foobar2")
- process.nextTick done
- before (done)->
- # Record participants
- Async.forEach ["8c0521ee", "c2659ef8", "be8bb5b1", "f3cb65e5", "6d9d70c5"],
- (id, done)->
- split.show id, done
- , (callback)->
- # Records completion
- Async.forEach ["8c0521ee", "f3cb65e5"],
- (id, done)->
- split.completed id, done
- , done
- before (done)->
- # Collect the results
- request.get test_url + "/data", (_, response)->
- { statusCode, body } = response
- result = JSON.parse(response.body) if statusCode == 200
- done()
+ it "should return historical data for each alternative", ->
+ assert a = test.alternatives[0].data
+ assert.equal a.length, 1
+ assert.equal a[0].participants, 2, "No participants for foo"
+ assert.equal a[0].converted, 0, "No test completed for foo"
- it "should return 200", ->
- assert.equal statusCode, 200
+ assert b = test.alternatives[1].data
+ assert.equal b.length, 1
+ assert.equal b[0].participants, 3, "No participatnts for bar"
+ assert.equal b[0].converted, 2, "No test completed for bar"
- it "should return historical data for each alternative", ->
- assert foo = result[0]
- assert.equal foo.length, 1
- assert.equal foo[0].participants, 2, "No participants for foo"
- assert.equal foo[0].converted, 0, "No test completed for foo"
- assert bar = result[1]
- assert.equal bar.length, 1
- assert.equal bar[0].participants, 3, "No participatnts for bar"
- assert.equal bar[0].converted, 2, "No test completed for bar"
View
4 server/views/split/show.eco
@@ -18,7 +18,9 @@ $(function() {
var DAYS = ["S", "M", "T", "W", "T", "F", "S"];
var testId = document.location.pathname.split("/").last();
- $.getJSON("/v1/split/" + testId + "/data", function(data) {
+ $.getJSON("/v1/split/" + testId, function(test) {
+ data = test.alternatives.map(function(alternative) { return alternative.data });
+ console.log(data)
d3.select("#alternatives")
.selectAll(".alternative").data(data)
.each(renderAlternative);

0 comments on commit b567769

Please sign in to comment.
Something went wrong with that request. Please try again.