Integrations

Seamlessly integrate Roundtable Alias into your existing survey platforms. Whether you’re using a custom solution or popular survey software like Forsta, Qualtrics, or jsPsych, our API provides robust behavioral tracking and fraud detection capabilities to ensure the integrity of your survey data.

Forsta

Integrate Roundtable Alias with your Forsta surveys to gain comprehensive insights into respondent behavior. With our seamless integration process, you can easily embed tracking code into your surveys and analyze data for fraudulent activity with precision.

1

Contact Forsta Support

Contact Forsta support and ask them to whitelist https://api.roundtable.ai for API calls.

2

Embed tracking code

In the XML for your survey, add the following lines of code to every “Text” or “Essay” questions you want to track (text and textarea elements in XML). This code embeds our Javascript tracker for the question and automatically extracts the question and response:

<style name="question.after"><![CDATA[<script src='https://roundtable.ai/js/forsta-tracker.js'></script>]]></style>
<style name="question.after" wrap="ready"><![CDATA[
const q = ${jsexport()};
const label = q['label']
const oes = document.querySelectorAll('input[type="text"], textarea');
oes.forEach(el => {
    el.addEventListener('input', e => handleChange(e, 'input', el, label, q));
    el.addEventListener('copy', e => handleChange(e, 'copy', el, label, q));
});
initializeClientDict(label, q);
]]></style>
3

Store data

Add the following lines of code to the end of your survey and set the api_key to your Alias API Key (you can generate keys on your account). This code will call the API on the page the code is embedded in. This generally takes a few seconds and must finish executing before the next page is loaded. Also note that the code must be placed after any open-ends you are tracking and on a separate page from any open-ends (i.e., after a <suspend/> in XML).

<exec>
survey_id = gv.survey.root.alt
api_key = ''
    
p.headers = {'api_key': api_key, 'Content-Type': 'application/json'}
p.data = {'responses': p.client_dict['responses'], 'questions': p.client_dict['questions'], 
'question_histories': p.client_dict['question_histories'], 'survey_id': survey_id, 'participant_id': uuid }
</exec>

<logic cond="p.client_dict['num_non_empty'] != 0" label="alias_api" api:data="p.data" api:headers="p.headers" api:method="POST" api:url="https://api.roundtable.ai/alias/latest" uses="api.1"/>
<exec>
def checks_array_to_string(array):
    return ', '.join(map(str, array)) if array else "None"

def format_checks(response, keys):
    return {key: checks_array_to_string(response.r.get('checks', {}).get(key, [])) for key in keys}

def format_response_groups(response, keys):
    return {key: response.r.get('response_groups', {}).get(key, 0) for key in keys}
        
labels = p.client_dict['question_labels']

alias_responses.r1.val = alias_api.r.get('flagged', False)
alias_responses.r2.val = format_checks(alias_api, labels)
alias_responses.r3.val = format_response_groups(alias_api,labels)
alias_responses.r4.val = alias_api.r.get('num_checks_failed', 0)
alias_responses.r5.val = alias_api.r.get('model', 'API not called')
alias_responses.r6.val = alias_api.r.get('error', False)
alias_responses.r7.val = alias_api.r.get('problem', 'None')
</exec>


<html label="h1">&lt;script&gt;
sessionStorage.removeItem('client_dict');
&lt;/script&gt;</html>

<text 
  label="alias_responses"
  optional="0"
  size="25"
  translateable="0"
  where="execute,survey,report">
  <title>roundtable data holder</title>
  <row label="r1">alias-flagged</row>
  <row label="r2">alias-checks</row>
  <row label="r3">alias-response_groups</row>
  <row label="r4">alias-num_checks_failed</row>
  <row label="r5">alias-model</row>
  <row label="r6">alias-error</row>
  <row label="r7">alias-problem</row>
</text>

The Alias response will now be stored in your survey data in the alias_responses field. The keys of the checks and response_groups objects will match the labels of the questions being tracked.

Example XML script

Here is a example of a simple survey with all of the Alias code integrated:

<?xml version="1.0" encoding="UTF-8"?>
<survey 
  alt="test-survey-1"
  autosave="0"
  builder:wizardCompleted="1"
  builderCompatible="1"
  compat="153"
  delphi="1"
  extraVariables="source,record,decLang,list,userAgent"
  fir="on"
  html:showNumber="0"
  mobile="compat"
  mobileDevices="smartphone,tablet,desktop"
  name="Survey"
  secure="1"
  setup="term,decLang,quota,time"
  ss:disableBackButton="1"
  ss:enableNavigation="1"
  ss:hideProgressBar="0"
  state="testing">

<samplesources default="0">
  <samplesource list="0">
    <title>Open Survey</title>
    <invalid>You are missing information in the URL. Please verify the URL with the original invite.</invalid>
    <completed>It seems you have already entered this survey.</completed>
    <exit cond="terminated">Thank you for taking our survey.</exit>
    <exit cond="qualified">Thank you for taking our survey. Your efforts are greatly appreciated!</exit>
    <exit cond="overquota">Thank you for taking our survey.</exit>
  </samplesource>
</samplesources>

<suspend/>

<text 
  label="q1"
  optional="1"
  size="25">
  <title>What do you do first in the morning?</title>
  <comment>Be specific</comment>
  <style name="question.after"><![CDATA[<script src='https://roundtable.ai/js/forsta-tracker.js'></script>]]></style>
  <style name="question.after" wrap="ready"><![CDATA[
  const q = ${jsexport()};
  const label = q['label']
  const oes = document.querySelectorAll('input[type="text"], textarea');
  oes.forEach(el => {
    el.addEventListener('input', e => handleChange(e, 'input', el, label, q));
	  el.addEventListener('copy', e => handleChange(e, 'copy', el, label, q));
	});
	initializeClientDict(label, q);
  ]]></style>
</text>

<suspend/>

<textarea 
  label="q2"
  height="10"
  optional="1"
  width="50">
  <title>Tell me about yourself</title>
  <comment>Be specific</comment>
  <style name="question.after"><![CDATA[
  <script src='https://roundtable.ai/js/forsta-tracker.js'></script>]]></style>
  <style name="question.after" wrap="ready"><![CDATA[
  const q = ${jsexport()};
  const label = q['label']
  const oes = document.querySelectorAll('input[type="text"], textarea');
  oes.forEach(el => {
    el.addEventListener('input', e => handleChange(e, 'input', el, label, q));
        el.addEventListener('copy', e => handleChange(e, 'copy', el, label, q));
  });
	initializeClientDict(label, q);
  ]]></style>
</textarea>

<suspend/>

<exec>
survey_id = gv.survey.root.alt
api_key = ''
    
p.headers = {'api_key': api_key, 'Content-Type': 'application/json'}
p.data = {'responses': p.client_dict['responses'], 'questions': p.client_dict['questions'], 
'question_histories': p.client_dict['question_histories'], 'survey_id': survey_id, 'participant_id': uuid }
</exec>

<logic cond="p.client_dict['num_non_empty'] != 0" label="alias_api" api:data="p.data" api:headers="p.headers" api:method="POST" api:url="https://api.roundtable.ai/api/alias/latest" uses="api.1"/>
<exec>
def checks_array_to_string(array):
    return ', '.join(map(str, array)) if array else "None"

def format_checks(response, keys):
    return {key: checks_array_to_string(response.r.get('checks', {}).get(key, [])) for key in keys}

def format_response_groups(response, keys):
    return {key: response.r.get('response_groups', {}).get(key, 0) for key in keys}
        
labels = p.client_dict['question_labels']

alias_responses.r1.val = alias_api.r.get('flagged', False)
alias_responses.r2.val = format_checks(alias_api, labels)
alias_responses.r3.val = format_response_groups(alias_api,labels)
alias_responses.r4.val = alias_api.r.get('num_checks_failed', 0)
alias_responses.r5.val = alias_api.r.get('model', 'API not called')
alias_responses.r6.val = alias_api.r.get('error', False)
alias_responses.r7.val = alias_api.r.get('problem', 'None')
</exec>


<html label="h1">&lt;script&gt;
sessionStorage.removeItem('client_dict');
&lt;/script&gt;</html>

<text 
  label="alias_responses"
  optional="0"
  size="25"
  translateable="0"
  where="execute,survey,report">
  <title>roundtable data holder</title>
  <row label="r1">alias-flagged</row>
  <row label="r2">alias-checks</row>
  <row label="r3">alias-response_groups</row>
  <row label="r4">alias-num_checks_failed</row>
  <row label="r5">alias-model</row>
  <row label="r6">alias-error</row>
  <row label="r7">alias-problem</row>
</text>
</survey>

Qualtrics

The Qualtrics integration works by adding a new column to your datasets with response behavior for each participant. This data is then fed to the Alias API at the end of the survey (or whenever you choose to pass the survey data to the API).

Follow these steps to integrate Alias with Qualtrics. Alternatively, you can important the survey template and adjust it as needed.

1

Add Javascript tracker

Our Qualtrics javascript tracker generates a history of every change made for each open-ended textbox. To use this tracker, simply paste the code into the “Javascript” block for any open-ended question (you must have at least one open-ended question to call the Alias API). title

2

Add an embedded data field

Add an embedded data field called alias_data to your survey. title

3

Run your survey

All of the data needed for the Alias API will be stored in the alias_data embedded data field.

4

Export your Qualtrics data

After running your survey, export your dataset from Qualtrics as a CSV. title

5

Upload to the Dashboard

Upload the exported CSV to the Alias Dashboard. This will call the Alias API for every tracked question and merge the results with your dataset. title

jsPsych

Empower your jsPsych experiments with Roundtable Alias to identify and mitigate fraudulent responses. Our integration seamlessly captures user behavior data, enabling in-depth analysis and fraud detection within your experimental settings.

1

Setup tracker

The Alias tracker integrates with JsPsych survey-text trials to identify potential fraudelent, inattentive, or bot-like responses to open-ended questions. You can use this as a naturalistic captcha to identify participants to exclude from your analyses.

The extension generates arrayss of all the change events to open-ended questions (called “question histories”), which you can then pass to our API. To use this extension, simply add a link to it in index.html, include it in the initJsPsych call, and then pass it as an extension to any jsPsychSurveyText questions you want to track (note that you must include at least one survey-text question with our extension).

The Alias tracker takes an optional initialization argument max_n_characters, when specifies the max number of characters the JSON string of each question_history can be (by default, this is 50,000; we highly recommend setting it to at least 20,000). The extension also requires a page_id parameter on every trial where the extension is used. This allows you to easily compare responses across participants even if there are conditional timelines or repeated questions.

We include a full example of using our extension in a JsPsych experiment in the public/ directory of our GitHub repository. Here is a simplified example:

const jsPsych = initJsPsych({
  extensions: [
    { type: jsPsychAliasTracker, params: { max_n_characters:55000 } }
  ],
});

...

const openEndsTrial = {
  type: jsPsychSurveyText,
  questions: [
    { prompt: 'What do you think was the purpose of this experiment?', required: true, rows: 6 },
  ],
  extensions: [
    { type: jsPsychAliasTracker, params: { page_id:'page1' } }
  ]
}

...
2

Call the API

Our tracker automatically adds all of the data the Alias API needs to trial data with the jsPsychAliasTracker extension. This data is stored as alias_questions, alias_responses, and alias_question_histories. This data can be passed to our API without any further modifications. An example of parsing the data from our example experiment and passing it to the API is included in call-api.py.

3

Deploy on Heroku

Deploy an Alias JsPsych experiment to Heroku, a cloud platform service. Follow the steps below to clone the repository, install necessary dependencies, and launch your experiment on Heroku.

git clone https://github.com/roundtableAI/alias-tracker.git  # Clones the repository
cd alias-tracker/                                            # Navigates into the directory
npm install                                                  # Installs all dependencies
heroku create your-app-name                                  # Creates a new Heroku app
git push heroku master                                       # Deploys the app to Heroku
heroku open                                                  # Opens the app in a web browser

Custom

For those with unique survey platforms or specific integration requirements, our API offers flexibility and adaptability. Follow our simple steps to integrate Roundtable Alias into your custom solution, allowing you to capture and analyze user behavior effectively.

Javascript

1

Setup tracker

Our Javascript tracker generates a history of every change made for each open-ended textbox. To use this tracker, replace the value of the textbox_id variable at the top of the script with the HTML id of the relevant textbox and then store the generated question_history. You can also specify the max number of characters the parsed question_history should take before it stops tracking (by default, this is set to 25,000). Note that as is, the script only works if there is one textbox per page.

const textbox_id = 'exampleTextarea'

// Max number of characters to store in history 
const max_characters_for_history = 25000;

document.addEventListener('DOMContentLoaded', function () {

    // Text changes are stored here
    let question_history = [];
    let old_text = document.getElementById(textbox_id).value;
    let text_over_length = false;
    let start_time;

    // Handle input changes
    document.getElementById(textbox_id).addEventListener('input', function (e) {
        if (text_over_length) return;
        if (!start_time) {
            start_time = Date.now();
            t = 0;
        } else {
            t = Date.now() - start_time;
        }
        const state = e.target.value;
        const new_history = {
            s: state,
            t,
        };
        const length_of_history = JSON.stringify([...question_history, new_history]).length;
        if (length_of_history > max_characters_for_history) {
            text_over_length = true;
            return;
        }
        question_history.push(new_history);
        old_text = state;
    });

    // Handle copy changes
    document.getElementById(textbox_id).addEventListener('copy', function (e) {
        if (text_over_length) return;
        if (!start_time) {
            start_time = Date.now();
            t = 0;
        } else {
            t = Date.now() - start_time;
        }
        const new_history = {
            s: e.target.value,
            t,
            o: 'c',
            ct: window.getSelection().toString(),
        };
        const length_of_history = JSON.stringify([...question_history, new_history]).length;
        if (length_of_history > max_characters_for_history) {
            text_over_length = true;
            return;
        }
        question_history.push(new_history);
    });
});
2

Call the API

Substitute the question_histories below with the data collected from the tracker above.

curl --request POST \
  --url https://api.roundtable.ai/alias/latest \
  --header 'Content-Type: application/json' \
  --header 'api_key: <api_key>' \
  --data '{
  "participant_id": "participant_123",
  "question_histories": {
    "Q1": [
      [
        {
          "s": "I",
          "t": 0
        },
        {
          "s": "Im",
          "t": 429
        }
      ]
    ]
  },
  "questions": {
    "Q1": "How do you feel our software has impacted your daily workflow?"
  },
  "responses": {
    "Q1": "I really like pineapple on pizza."
  },
  "survey_id": "survey_456"
}'

React

1

Setup tracker

import { useState } from 'react';

const useQuestionHistory = (maxCharactersForHistory = 25000) => {
    const [history, setHistory] = useState([]);
    const [textOverLength, setTextOverLength] = useState(false);
    const [startTime, setStartTime] = useState(null);

    const getTimeElapsed = () => {
        const currentTime = Date.now();
        if (!startTime) {
            setStartTime(currentTime);
            return 0;
        }
        return currentTime - startTime;
    };

    const addHistoryEntry = (newEntry) => {
        if (textOverLength) return;

        const updatedHistory = [...history, newEntry];
        const lengthOfHistory = JSON.stringify(updatedHistory).length;

        if (lengthOfHistory > maxCharactersForHistory) {
            setTextOverLength(true);
            return;
        }

        setHistory(updatedHistory);
    };

    const handleInput = (event) => {
        const timeElapsed = getTimeElapsed();
        const newEntry = {
            s: event.target.value,
            t: timeElapsed
        };
        addHistoryEntry(newEntry);
    };

    const handleCopy = (event) => {
        const timeElapsed = getTimeElapsed();
        const newEntry = {
            s: event.target.value,
            t: timeElapsed,
            o: 'c',
            ct: window.getSelection().toString()
        };
        addHistoryEntry(newEntry);
    };

    return { history, handleInput, handleCopy };
};

export default useQuestionHistory;
2

Use it in an open-end component

import useQuestionHistory from './useQuestionHistory';
const TextboxComponent = () => {
    const { history, handleInput, handleCopy } = useQuestionHistory();
    return (
        <textarea
            id="exampleTextarea"
            onChange={handleInput}
            onCopy={handleCopy}
        />
    );
};
export default TextboxComponent;
3

Call the API