About measuring response time in an online experiment with timestamp

288 views
Skip to first unread message

傅韋銘

unread,
Feb 24, 2022, 5:28:21 PM2/24/22
to oTree help & discussion
Dear all,
I am now writing an app with otree for an online experiment that I need to record response time. We now use Heroku as server and plan to implement the experiment on Amazon Turk. I face some issues that I hope to get help with.

1. I am now using the difference of two timestamps to record the response time on the client side. Since we hope the accuracy of response time is at the level of milisecond. I am not quite sure if there exists any possible delay getting timestamp from client to server. To be specific, I want to make sure even if there exists a latency, will the latency be equal in each timestamp or not.

2. I did a test using the Heroku server for my app. I found that if the subject close the Web Browser during the experiment, the time record function will still work even browser is closed. As they reopen the browser,  the timestamp will add on the duration when they are not in the task. which will lead to an extra longer and wrong response time.
I am wondering if there is any way I can avoid this problem through coding.

Thank you very much in advance for your help.

Sincerely,
Wei-ming

Chris @ oTree

unread,
Feb 24, 2022, 11:49:04 PM2/24/22
to 傅韋銘, oTree help & discussion
Can you show your code?

Sent from my phone

On Feb 25, 2022, at 6:28 AM, 傅韋銘 <colin...@gmail.com> wrote:

Dear all,
--
You received this message because you are subscribed to the Google Groups "oTree help & discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to otree+un...@googlegroups.com.
To view this discussion on the web, visit https://groups.google.com/d/msgid/otree/e1150e51-5b69-47cd-bda6-0fd860002262n%40googlegroups.com.

傅韋銘

unread,
Feb 28, 2022, 6:23:30 AM2/28/22
to oTree help & discussion
Dear Chris,
Sorry for the late reply. (Sorry that I did not forward to all in forum initially). I set up two fields for timestamp and the time recording function is in the live method on the Game page.

The following is my initial py. code:

from random import random
from otree.api import *
from otree.models import subsession


doc = """
Your app description
"""


class Constants(BaseConstants):
name_in_url = 'monty_hall'
players_per_group = None
num_rounds = 10
num_doors = 3
num_rows = 1
win_payoff = cu(10)
commit_payoff = cu(1)
# lose_payoff = cu(0)


class Subsession(BaseSubsession):
answer = models.IntegerField()

class Group(BaseGroup):
pass

class Player(BasePlayer):
is_winner = models.BooleanField(initial=False)
first_choice = models.IntegerField(initial=101)
old_choice = models.IntegerField(initial=101)
final_choice = models.IntegerField(initial=101)
switch = models.BooleanField()
confirm = models.BooleanField(initial=False)
game_over = models.BooleanField(initial=False)
open = models.BooleanField(initial=False)
second_stage = models.BooleanField(initial=False)
first_stage_correct = models.BooleanField(initial=False)
door_keep = models.IntegerField(initial=101)
commit = models.IntegerField(blank=True,
choices=[-1, 0, 1, 2],
widget=widgets.RadioSelect)
temp_payoff = models.CurrencyField(initial=cu(0))
hollow = models.BooleanField(initial=True)
first_door_timestamp = models.FloatField()
reveal_timestamp = models.FloatField()
submit_timestamp = models.FloatField()




def creating_session(subsession: Subsession):
import random
subsession.answer = random.randint(1, Constants.num_doors)



class Game(Page):
def is_displayed(player):
# load commit from participant field
participant = player.participant
if player.round_number == 1 and player.game_over == False:
participant.vars['monty_hall_cmt'] = 0
player.commit = participant.vars['monty_hall_cmt']
return player

@staticmethod
def js_vars(player: Player):
subsession = player.subsession
return dict(num_doors = Constants.num_doors,
num_rows = Constants.num_rows,
answer = subsession.answer,
commit = player.commit)

def live_method(player: Player, data):
import random
subsession = player.subsession
participant = player.participant
print('data:', data)

if data:
if ('door' in data.keys()):
if (data and data['door'] in range(1,101)):
if (player.second_stage == False):
player.old_choice = player.first_choice
player.first_choice = data['door']
else:
player.old_choice = player.final_choice
player.final_choice = data['door']
player.confirm = True
player.hollow = False

if ('status' in data.keys()):
if (data and data['status'] == 'open'):
player.open = True
# temporarily set final_choice as first_choice, player can switch later
player.final_choice = player.first_choice
player.old_choice = player.first_choice
if player.first_choice == subsession.answer:
player.first_stage_correct = True
player.door_keep = player.first_choice
while player.door_keep == player.first_choice:
player.door_keep = random.randint(1, Constants.num_doors)
else:
player.door_keep = subsession.answer

if (data and data['status'] == 'opened'):
player.second_stage = True

if (data and data['status'] == 'final_choice'):
if player.final_choice == subsession.answer:
player.is_winner = True
player.payoff += Constants.win_payoff
if player.commit != 0:
player.payoff += Constants.commit_payoff

player.first_door_timestamp = data['first_door_timestamp']
player.reveal_timestamp = data['reveal_timestamp']
player.submit_timestamp = data['submit_timestamp'
]

player.game_over = True
# Record whether the player switched
if player.first_choice == player.final_choice:
player.switch = False
else:
player.switch = True


print("answer:", subsession.answer)
print("door keep:", player.door_keep)
print("old choice:", player.old_choice)
print("first choice:", player.first_choice)
print("final choice:", player.final_choice)
print("old choice:", player.old_choice)
print("second stage:", player.second_stage)
print("commit:", player.commit)
print("----------------")

return {
player.id_in_group: dict(
game_over=player.game_over,
first_choice = player.first_choice,
final_choice = player.final_choice,
old_choice = player.old_choice,
open = player.open,
second_stage = player.second_stage,
door_keep = player.door_keep,
commit = participant.monty_hall_cmt,
confirm = player.confirm,
hollow = player.hollow
)
}



class ResultsWaitPage(WaitPage):
pass

class Commit(Page):
form_model = 'player'
form_fields = ['commit']

def is_displayed(player):
return player.commit == 0

def before_next_page(player: Player, timeout_happened):
if player.commit in [1,2]:
participant = player.participant
participant.monty_hall_cmt = player.commit

@staticmethod
def error_message(player, values):
if values['commit'] == 0:
return 'Please answer whether you want to commit.'
def js_vars(player: Player):
return dict(switch = player.switch)

class Results(Page):
pass


page_sequence = [Game, Results, Commit]

The following is html and javascript code for game page.{{ block title }}
Main Game
{{ endblock }}

{{ block content }}

<link rel="stylesheet" href="{{ static 'CSS/Monty_hall_style.css' }}">

<p id='commit' style="color:red; font-size: 26px;"></p>

<p id='guide'>
Pick a door and click "Reveal a goat".
</p>

<div id="door_series"></div>

<button id="next" type="button" class="btn btn-primary" onclick="liveSend({'status': 'open'});reveal_timestamp = performance.now()">Reveal a goat</button>

<script>
var width_unit = (720*js_vars.num_rows)/(js_vars.num_doors*12)

// Create doors
for (let i = 1; i < js_vars.num_doors+1; i++) {
var bg = document.createElement("div");
if (i == js_vars.answer){
bg.className='answer'
} else {
bg.className='door_bg'
}
bg.style.width = width_unit*10 + 'px';
bg.style.height = width_unit*10 + 'px';
bg.style.marginLeft = width_unit + 'px';
bg.style.marginRight = width_unit + 'px';

var door = document.createElement("div");
door.className='door';
door.id = i;
door.style.width = width_unit*10 + 'px';
door.style.height = width_unit*10 + 'px';
door.onclick = function() { liveSend({'door': i}); }

bg.appendChild(door)
document.getElementById("door_series").appendChild(bg);
}

function toggleDoor() {
element.classList.toggle("doorOpen");
};

let next = document.getElementById('next');
let commit = document.getElementById('commit');
let first_door_timestamp;
let reveal_timestamp;
let second_door_timestamp;

let submit_timestamp;

if (js_vars.commit == 0) {
commit.innerText = ''
} else if (js_vars.commit == 1) {
commit.innerText = 'You have commited to \"switch\".'
} else {
commit.innerText = 'You have commited to \"stay\".'
}


// live section
function liveRecv(data) {
// open all doors when the game is over
if (data.game_over === true) {
next.btnDisabledStatus = 'disabled';
next.style.visibility = 'hidden';
for (let i = 1; i < js_vars.num_doors+1; i++){
document.getElementById(i).classList.add("doorOpen");
};
setTimeout(() => { document.getElementById('form').submit(); }, 2000);
};

let current_choice;
if (data.second_stage === false){
if (data.first_choice != 101 && data.old_choice == 101){
first_door_timestamp = performance.now()
};
current_choice = data.first_choice;
} else {
// if (final_choice == 101){
// second_door_timestamp = performance.now();
// };
current_choice = data.final_choice;
};

if (data.second_stage === false) {
if (data.first_choice == 101) {
next.disabled = 'disabled';
} else {
next.disabled = '';
}
} else {
next.textContent = 'Submit';
next.onclick = function(){ liveSend({
'status': 'final_choice',
'first_door_timestamp': first_door_timestamp,
'reveal_timestamp': reveal_timestamp,
'submit_timestamp': performance.now()
});}
if (data.commit == 0) {
document.getElementById('guide').innerHTML = "You can stay with your current choice or switch to another door.<br>\
Make your final choice and click \"Submit\"";
}
// commit == 2 mean "stay"
if (data.commit == 2) {
document.getElementById('guide').innerHTML = "Since you have commited to \"Stay\", you can not change your answer.<br>\
Click \"Submit\" to proceed.";
door = document.getElementById(data.door_keep);
door.onclick = function() {
alert(
'You cannot change your choice because you have committed to always stay. Click next to proceed.'
);
};
};
// commit == 1 mean "switch"
if (data.commit == 1) {
document.getElementById('guide').innerHTML = "Since you have commited to \"Switch\", you must change your answer if posible.<br>\
Click the other door and then \"Submit\" to proceed.";
document.getElementById(data.first_choice).onclick = function() {
alert(
'You cannot select the door you chose in the first stage as your final choice because you have committed to always switch.'
);
};

};
if (data.confirm == false) {
next.disabled = 'disabled';
} else {
next.disabled = false;
};
};

if (data.open == true && data.second_stage == false) {
var keep = data.door_keep;
var first_choice = data.first_choice;

for (let i = 1; i < js_vars.num_doors+1; i++) {
if (i != keep && i != first_choice) {
door = document.getElementById(i);
door.classList.add("doorOpen");
door.onclick = null;
};
};
liveSend({'status': 'opened'});
};

// select a door, add the check mark
if (current_choice != 101) {
if (data.old_choice != 101) {
old_choice = document.getElementById(data.old_choice);
// old_choice.style.borderColor = "orange";
old_choice.removeChild(document.getElementById(data.old_choice+1000))
};
selected_door = document.getElementById(current_choice);
// selected_door.style.borderColor = "blue";
var check = document.createElement("span");
if (data.hollow == true) {
check.className='hollow_checkmark';
} else {
check.className='checkmark';
}

check.id = current_choice + 1000;
check.style.width = width_unit*9 + 'px';
check.style.height = width_unit*9 + 'px';
selected_door.appendChild(check);
};


};


window.addEventListener('DOMContentLoaded', (event) => {
liveSend(null);
});
</script>

{{ endblock }}

Thank you very much for your help.

Sincerely,
Wei-ming

Chris @ oTree 在 2022年2月25日 星期五下午12:49:04 [UTC+8] 的信中寫道:
Reply all
Reply to author
Forward
0 new messages