Skip to content

Commit 657a6ab

Browse files
feat(qualtrics): support dynamic reference params in Qualtrics element (#1217)
* feat(qualtrics): support dynamic reference params in Qualtrics element (#1211) Qualtrics params now accept `reference` and `position` fields (same as TrackedLink) so researchers can pass player state — urlParams, participantInfo, prompt answers, etc. — directly into the survey URL at render time. - Qualtrics.jsx: resolves reference params via referenceResolver (same logic as TrackedLink); static `value` params continue to work unchanged - validateTreatmentFile.ts: qualtrics params now validated with trackedLinkParamSchema (key + optional value or reference + position) - Added 6 Playwright component tests (QURL-001–006) covering static params, always-present deliberationId/sampleId, urlParams reference, participantInfo reference, mixed params, and QualtricsEOS submission Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(qualtrics): document reference params support (#1211) Update qualtrics.md, page-elements.md, and syntax-reference.md to reflect the new `reference`/`position` fields in params. Add examples for static, dynamic, and mixed param configurations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(qualtrics): move qualtricsSchema after trackedLinkParamSchema to fix TDZ error qualtricsSchema referenced trackedLinkParamSchema before it was defined, causing a ReferenceError at module load time that crashed batch creation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(qualtrics): rename params → urlParams to match TrackedLink API Harmonize the Qualtrics element API with TrackedLink by renaming the `params` field to `urlParams` across client, server validation, tests, fixtures, and docs. Only the completed SDC_behavior study was using the old name, so no active experiments are affected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(elements): extract shared URL param utils, fix warning + URL builder - Extract serializeParamValue and pickFirstDefined into urlParamUtils.js so Qualtrics and TrackedLink share one implementation (addresses Copilot comment on duplication) - Fix warning condition: fire only when pickedValue is undefined, not when resolvedValue is "" (which is a valid empty-string param value) - Build Qualtrics fullURL via new URL() + searchParams.append so existing query params in the base URL are preserved and invalid URLs throw early (addresses Copilot double-? comment and user's URL format check request) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4174001 commit 657a6ab

11 files changed

Lines changed: 382 additions & 52 deletions

File tree

client/src/elements/Qualtrics.jsx

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
/* eslint-disable guard-for-in */
2-
/* eslint-disable no-restricted-syntax */
3-
4-
import React, { useEffect, useReducer } from "react";
5-
import { usePlayer } from "@empirica/core/player/classic/react";
1+
import React, { useEffect, useMemo, useReducer } from "react";
2+
import {
3+
useGame,
4+
usePlayer,
5+
usePlayers,
6+
} from "@empirica/core/player/classic/react";
67
import { useIdleContext } from "../components/IdleProvider";
78
import { useProgressLabel } from "../components/progressLabel";
9+
import { resolveReferenceValues as resolveReferences } from "../components/referenceResolver";
10+
import { serializeParamValue, pickFirstDefined } from "./urlParamUtils";
811

9-
export function Qualtrics({ url, params, onSubmit }) {
12+
export function Qualtrics({ url, urlParams, onSubmit }) {
13+
const game = useGame();
1014
const player = usePlayer();
15+
const players = usePlayers();
1116
const { setAllowIdle } = useIdleContext();
1217
const progressLabel = useProgressLabel();
1318
const deliberationId = player?.get("participantData")?.deliberationId;
@@ -52,7 +57,7 @@ export function Qualtrics({ url, params, onSubmit }) {
5257
return newState;
5358
};
5459

55-
const [state, dispatch] = useReducer(reducer, {
60+
const [, dispatch] = useReducer(reducer, {
5661
qualtricsSubmitted: false,
5762
});
5863

@@ -66,18 +71,42 @@ export function Qualtrics({ url, params, onSubmit }) {
6671
};
6772
}, [url, onSubmit, progressLabel]);
6873

69-
let fullURL = url;
74+
const resolvedParams = useMemo(() => {
75+
if (!urlParams) return [];
76+
return urlParams.map((param) => {
77+
if (!param.reference) {
78+
return {
79+
key: param.key,
80+
value: param.value === undefined ? "" : serializeParamValue(param.value),
81+
};
82+
}
83+
const referenceValues = resolveReferences({
84+
reference: param.reference,
85+
position: param.position,
86+
player,
87+
game,
88+
players,
89+
});
90+
const pickedValue = pickFirstDefined(referenceValues);
91+
const resolvedValue =
92+
pickedValue === undefined ? "" : serializeParamValue(pickedValue);
93+
if (pickedValue === undefined && referenceValues?.length) {
94+
console.warn(
95+
`Qualtrics: reference ${param.reference} resolved to undefined.`,
96+
referenceValues
97+
);
98+
}
99+
return { key: param.key, value: resolvedValue };
100+
});
101+
}, [game, urlParams, player, players]);
70102

71-
const paramsObj = new URLSearchParams();
72-
if (params) {
73-
for (const { key, value } of params) {
74-
paramsObj.append(key, value);
75-
}
76-
}
77-
paramsObj.append("deliberationId", deliberationId); // deliberationId is always passed so that we can link qualtrics responses to participants within qualtrics data
78-
paramsObj.append("sampleId", sampleId); // sampleId is always passed so that we can link qualtrics responses to participants within qualtrics data
79-
fullURL = `${url}?${paramsObj.toString()}`;
80-
console.log("fullURL", fullURL);
103+
const fullURL = useMemo(() => {
104+
const urlObj = new URL(url);
105+
resolvedParams.forEach(({ key, value }) => urlObj.searchParams.append(key, value));
106+
urlObj.searchParams.append("deliberationId", deliberationId); // always passed to link qualtrics responses to participants
107+
urlObj.searchParams.append("sampleId", sampleId); // always passed to link qualtrics responses to participants
108+
return urlObj.toString();
109+
}, [url, resolvedParams, deliberationId, sampleId]);
81110

82111
return (
83112
<div

client/src/elements/TrackedLink.jsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useProgressLabel,
1111
useGetElapsedTime,
1212
} from "../components/progressLabel";
13+
import { serializeParamValue, pickFirstDefined } from "./urlParamUtils";
1314

1415
function ExternalLinkIcon({ className = "h-4 w-4" }) {
1516
return (
@@ -25,15 +26,6 @@ function ExternalLinkIcon({ className = "h-4 w-4" }) {
2526
);
2627
}
2728

28-
const serializeParamValue = (value) => {
29-
if (value === undefined || value === null) return "";
30-
if (typeof value === "boolean") return value ? "true" : "false";
31-
return value.toString();
32-
};
33-
34-
const pickFirstDefined = (values) =>
35-
values?.find((val) => val !== undefined && val !== null);
36-
3729
/**
3830
* Instrumented external link element.
3931
* - Opens the destination in a new tab
@@ -137,7 +129,7 @@ export function TrackedLink({ name, url, urlParams = [], displayText }) {
137129
const resolvedValue =
138130
pickedValue === undefined ? "" : serializeParamValue(pickedValue);
139131

140-
if (resolvedValue === "" && referenceValues?.length) {
132+
if (pickedValue === undefined && referenceValues?.length) {
141133
console.warn(
142134
`TrackedLink ${name}: reference ${param.reference} resolved to undefined.`,
143135
referenceValues
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Shared URL parameter utilities used by Qualtrics and TrackedLink.
3+
*/
4+
5+
export const serializeParamValue = (value) => {
6+
if (value === undefined || value === null) return "";
7+
if (typeof value === "boolean") return value ? "true" : "false";
8+
return value.toString();
9+
};
10+
11+
export const pickFirstDefined = (values) =>
12+
values?.find((val) => val !== undefined && val !== null);

cypress/fixtures/mockCDN/projects/example/cypress.treatments.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ treatments:
697697
elements:
698698
- type: qualtrics
699699
url: https://upenn.co1.qualtrics.com/jfe/form/SV_cumihDjKknDL702
700-
params:
700+
urlParams:
701701
- key: dummyData
702702
value: "this is it!"
703703

cypress/fixtures/mockCDN/projects/example/demo.treatments.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ treatments:
5757
elements:
5858
- type: qualtrics
5959
url: https://upenn.co1.qualtrics.com/jfe/form/SV_cumihDjKknDL702
60-
params:
60+
urlParams:
6161
- key: dummyData
6262
value: "this is it!"
6363
- name: Stars Video

docs/study-design/page-elements.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,24 @@ See [Prompt Files](prompts.md) for explanations of the various types of prompt a
4747
4848
## Qualtrics
4949
50-
This component embeds a Qualtrics survey inside an iframe in the current stage display. This is intended primarily as an escape hatch to allow study designers to use Qualtrics surveys to implement parts of the experiment that they Deliberation Lab does not yet support. Provide `url` and optional `params` (list of `{key, value}` objects). The component automatically appends `deliberationId` and `sampleId`, listens for the Qualtrics end-of-survey message, and records the session metadata. Completing the Qualtrics survey submits the stage, so no separate submit button is required.
50+
This component embeds a Qualtrics survey inside an iframe in the current stage display. This is intended primarily as an escape hatch to allow study designers to use Qualtrics surveys to implement parts of the experiment that Deliberation Lab does not yet support. Provide `url` and optional `urlParams`. The component automatically appends `deliberationId` and `sampleId`, listens for the Qualtrics end-of-survey message, and records the session metadata. Completing the Qualtrics survey submits the stage, so no separate submit button is required.
5151

52-
In your qualtrics survey, make sure to either collect the deliberationId from the url parameter, or ask for participant's identifiers, so you can match data across platforms.
52+
In your Qualtrics survey, make sure to either collect the `deliberationId` from the URL parameter, or ask for participant identifiers, so you can match data across platforms.
53+
54+
Optional `urlParams` let you append literal query parameters or reference values captured elsewhere in the study. Each parameter accepts:
55+
56+
- `key`: required query parameter name.
57+
- `value`: optional literal string/number/boolean.
58+
- `reference`: optional [Reference Syntax](reference-syntax.md) pointer instead of `value` — resolved per-participant at render time. You can also pass `position` if you need a different subject (defaults to `player` when omitted).
5359

5460
```yaml
5561
- type: qualtrics
5662
url: https://upenn.qualtrics.com/jfe/form/SV_xxx
57-
params:
63+
urlParams:
5864
- key: condition
5965
value: topicA
66+
- key: prolificId
67+
reference: urlParams.PROLIFIC_PID
6068
```
6169

6270
## Tracked Link

docs/study-design/qualtrics.md

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,58 @@ In a treatment file:
1919
elements:
2020
- type: qualtrics
2121
url: https://yourdc.qualtrics.com/jfe/form/SV_ABC123
22-
# optional: additional query params to append
23-
params:
24-
- customKey: customValue
22+
urlParams:
23+
- key: condition
24+
value: treatment
2525
```
2626
2727
Rules and behavior:
2828
29-
- `url` is required; `params` is optional and is an array of key/value maps.
29+
- `url` is required; `urlParams` is optional.
3030
- At runtime, Deliberation Lab **automatically appends** `deliberationId` and `sampleId` as query parameters so you can join Qualtrics data to the science export even if API fetches are disabled.
3131
- If `QUALTRICS_API_TOKEN` or `QUALTRICS_DATACENTER` is missing, batch initialization fails when validating treatments.
3232

33+
## Passing URL parameters
34+
35+
Each entry in `urlParams` accepts:
36+
37+
- `key`: required — the query parameter name.
38+
- `value`: optional literal string, number, or boolean.
39+
- `reference`: optional [Reference Syntax](reference-syntax.md) pointer resolved from player/game state at render time. Cannot be combined with `value`.
40+
- `position`: optional position selector (`player`, `shared`, `all`, etc.) for the reference lookup — defaults to `player`.
41+
42+
**Static value** (same for all participants):
43+
44+
```yaml
45+
urlParams:
46+
- key: condition
47+
value: treatment-A
48+
```
49+
50+
**Dynamic reference** (resolved per-participant at render time):
51+
52+
```yaml
53+
urlParams:
54+
- key: prolificId
55+
reference: urlParams.PROLIFIC_PID
56+
- key: participantName
57+
reference: participantInfo.name
58+
```
59+
60+
**Mixed** (static and dynamic together):
61+
62+
```yaml
63+
urlParams:
64+
- key: condition
65+
value: treatment-A
66+
- key: prolificId
67+
reference: urlParams.PROLIFIC_PID
68+
- key: surveyAnswer
69+
reference: prompt.topicChoice
70+
```
71+
72+
Any reference namespace supported by the platform works here — `urlParams`, `participantInfo`, `connectionInfo`, `browserInfo`, `prompt`, `survey`, etc. See [Reference Syntax](reference-syntax.md) for the full list.
73+
3374
## What participants see
3475

3576
- The survey is embedded in an iframe sized for the stage.

docs/study-design/syntax-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Invalid types or missing name/path emit validation errors.
9090
- `image`: `type: image`, `file` required.
9191
- `display`: `type: display`, `reference` required (see §4), `position` selector (`shared` | `player` | `all` | `any` | int; default `player`).
9292
- `prompt`: `type: prompt`, `file` required, `shared?` (true for shared prompt data; disallowed in intro/exit).
93-
- `qualtrics`: `type: qualtrics`, `url` required (survey link), `params?` array of key/value maps. Runtime: env vars `QUALTRICS_API_TOKEN` and `QUALTRICS_DATACENTER` are required at validation time; Deliberation Lab appends `deliberationId` and `sampleId` to the URL automatically.
93+
- `qualtrics`: `type: qualtrics`, `url` required (survey link), `urlParams?` array of param objects — each with `key` (required), and either `value` (literal string/number/boolean) or `reference` (reference string) plus optional `position`; `value` and `reference` are mutually exclusive. Runtime: env vars `QUALTRICS_API_TOKEN` and `QUALTRICS_DATACENTER` are required at validation time; Deliberation Lab appends `deliberationId` and `sampleId` to the URL automatically.
9494
- `separator`: `type: separator`, `style?` enum `thin | thick | regular`.
9595
- `sharedNotepad`: `type: sharedNotepad`.
9696
- `submitButton`: `type: submitButton`, `buttonText?` (<=50 chars).
@@ -159,7 +159,7 @@ Invalid types or missing name/path emit validation errors.
159159
## 14) Runtime Semantics and Interop Notes
160160

161161
- **Template expansion** happens before validation is applied to the final structures used at runtime (`getTreatments` + `fillTemplates`). Unresolved `${...}` cause errors.
162-
- **Qualtrics elements** require `QUALTRICS_API_TOKEN` and `QUALTRICS_DATACENTER` env vars; validation will throw if missing. At runtime, Deliberation Lab appends `deliberationId` and `sampleId` as URL params; submitted Qualtrics responses are fetched (if API keys) and stored under `qualtrics_<step>` in science data.
162+
- **Qualtrics elements** require `QUALTRICS_API_TOKEN` and `QUALTRICS_DATACENTER` env vars; validation will throw if missing. At runtime, Deliberation Lab appends `deliberationId` and `sampleId` as URL params; any `urlParams` entries are also appended (with `reference` values resolved from player/game state); submitted Qualtrics responses are fetched (if API keys) and stored under `qualtrics_<step>` in science data.
163163
- **Survey elements** rely on `@watts-lab/surveys`; ensure `surveyName` is valid there.
164164
- **Discussion/video** layouts control Daily call composition; `rooms` split participants across subrooms; `layout` defines on-screen tiling for video stages.
165165
- **Visibility/conditions** are evaluated in the client to gate rendering of prompts, displays, etc.; make sure referenced data exists in earlier steps or URL/browser/connection info.

0 commit comments

Comments
 (0)