Skip to content

Commit b9ec0fb

Browse files
authored
Merge pull request #47 from Plurality-Institute/raindrift/bri-268-collect-bot-detection-data-merge
Collect bot detection metadata
2 parents e077b23 + d92c53d commit b9ec0fb

22 files changed

+779
-65
lines changed

eslint.config.mjs

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,65 @@
1-
import typescriptEslint from "@typescript-eslint/eslint-plugin";
2-
import globals from "globals";
3-
import tsParser from "@typescript-eslint/parser";
4-
import path from "node:path";
5-
import { fileURLToPath } from "node:url";
6-
import js from "@eslint/js";
7-
import { FlatCompat } from "@eslint/eslintrc";
1+
import typescriptEslint from '@typescript-eslint/eslint-plugin';
2+
import globals from 'globals';
3+
import tsParser from '@typescript-eslint/parser';
4+
import path from 'node:path';
5+
import {fileURLToPath} from 'node:url';
6+
import js from '@eslint/js';
7+
import {FlatCompat} from '@eslint/eslintrc';
88

99
const __filename = fileURLToPath(import.meta.url);
1010
const __dirname = path.dirname(__filename);
1111
const compat = new FlatCompat({
12-
baseDirectory: __dirname,
13-
recommendedConfig: js.configs.recommended,
14-
allConfig: js.configs.all
12+
baseDirectory: __dirname,
13+
recommendedConfig: js.configs.recommended,
14+
allConfig: js.configs.all,
1515
});
1616

17-
export default [{
17+
export default [
18+
{
1819
ignores: [
19-
"**/dist/",
20-
"**/node_modules/",
21-
"**/webpack.config.js",
22-
"**/lit-css-loader.js",
20+
'**/dist/',
21+
'**/node_modules/',
22+
'**/webpack.config.js',
23+
'**/lit-css-loader.js',
2324
],
24-
}, ...compat.extends(
25-
"eslint:recommended",
26-
"plugin:@typescript-eslint/eslint-recommended",
27-
"plugin:@typescript-eslint/recommended",
28-
), {
25+
},
26+
...compat.extends(
27+
'eslint:recommended',
28+
'plugin:@typescript-eslint/eslint-recommended',
29+
'plugin:@typescript-eslint/recommended',
30+
),
31+
{
2932
plugins: {
30-
"@typescript-eslint": typescriptEslint,
33+
'@typescript-eslint': typescriptEslint,
3134
},
3235

3336
languageOptions: {
34-
globals: {
35-
...globals.browser,
36-
},
37+
globals: {
38+
...globals.browser,
39+
},
3740

38-
parser: tsParser,
39-
ecmaVersion: 2020,
40-
sourceType: "module",
41+
parser: tsParser,
42+
ecmaVersion: 2020,
43+
sourceType: 'module',
4144
},
4245

4346
rules: {
44-
"no-case-declarations": "off",
45-
"no-prototype-builtins": "off",
46-
"@typescript-eslint/ban-types": "off",
47-
"@typescript-eslint/explicit-function-return-type": "off",
48-
"@typescript-eslint/explicit-module-boundary-types": "off",
49-
"@typescript-eslint/no-explicit-any": "error",
50-
"@typescript-eslint/no-empty-function": "off",
51-
"@typescript-eslint/no-non-null-assertion": "off",
52-
"@typescript-eslint/no-unused-vars": ["warn", {
53-
argsIgnorePattern: "^_",
54-
}],
47+
'no-case-declarations': 'off',
48+
'no-prototype-builtins': 'off',
49+
'@typescript-eslint/ban-types': 'off',
50+
'@typescript-eslint/explicit-function-return-type': 'off',
51+
'@typescript-eslint/explicit-module-boundary-types': 'off',
52+
'@typescript-eslint/no-explicit-any': 'error',
53+
'@typescript-eslint/no-empty-function': 'off',
54+
'@typescript-eslint/no-non-null-assertion': 'off',
55+
'@typescript-eslint/no-unused-vars': [
56+
'warn',
57+
{
58+
argsIgnorePattern: '^_',
59+
varsIgnorePattern: '^_',
60+
caughtErrorsIgnorePattern: '^_',
61+
},
62+
],
5563
},
56-
}];
64+
},
65+
];

firestore/firestore.rules

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ service cloud.firestore {
113113
match /agents/{agentId} {
114114
allow get: if isExperimenter();
115115
allow list: if isExperimenter();
116-
// Experimenters can use cloud function endpoints
116+
// Experimenters can use cloud function endpoints
117117
allow write: if false;
118118

119119
match /chatPrompts/{promptId} {
@@ -131,13 +131,13 @@ service cloud.firestore {
131131
allow get: if true; // Public read
132132
allow list: if canEditExperiment(experimentId);
133133

134-
// Experimenters can use cloud function endpoints
134+
// Experimenters can use cloud function endpoints
135135
allow write: if false;
136136

137137
match /publicStageData/{stageId} {
138138
allow read: if true;
139139

140-
// TODO: Triggered by cloud function triggers
140+
// TODO: Triggered by cloud function triggers
141141
allow write: if false;
142142

143143
match /chats/{chatId} {
@@ -169,7 +169,7 @@ service cloud.firestore {
169169
allow get: if true;
170170
allow list: if true;
171171

172-
// Use cloud function endpoints
172+
// Use cloud function endpoints
173173
allow update: if false;
174174
}
175175

@@ -180,27 +180,33 @@ service cloud.firestore {
180180
allow list: if true;
181181
allow get: if true; // Public read
182182

183-
// Participants can use cloud function endpoints
183+
// Participants can use cloud function endpoints
184184
allow update: if false;
185185

186186
match /stageData/{stageId} {
187187
allow read: if true;
188188

189-
// Participants can use cloud function endpoints
189+
// Participants can use cloud function endpoints
190190
allow write: if false;
191191
}
192192

193193
match /alerts/{alertId} {
194194
allow read: if true;
195195
allow write: if false; // Use cloud functions
196196
}
197+
198+
// Append-only participant behavior events
199+
match /behavior/{eventId} {
200+
allow read: if isExperimenter();
201+
allow write: if false; // Use cloud functions
202+
}
197203
}
198204

199205
match /participantPublicData/{participantPublicId} {
200206
allow list: if true;
201207
allow get: if true;
202208

203-
// Triggered by cloud function triggers
209+
// Triggered by cloud function triggers
204210
allow write: if false;
205211
}
206212
}

frontend/src/components/experiment_builder/experiment_settings_editor.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import {MobxLitElement} from '@adobe/lit-mobx';
22
import {CSSResultGroup, html, nothing} from 'lit';
3-
import {customElement, property} from 'lit/decorators.js';
3+
import {customElement} from 'lit/decorators.js';
44

55
import '@material/web/textfield/filled-text-field.js';
66
import '@material/web/checkbox/checkbox.js';
77

88
import {core} from '../../core/core';
9-
import {ButtonClick, AnalyticsService} from '../../services/analytics.service';
10-
import {AuthService} from '../../services/auth.service';
11-
import {HomeService} from '../../services/home.service';
12-
import {Pages, RouterService} from '../../services/router.service';
139
import {ExperimentEditor} from '../../services/experiment.editor';
1410
import {ExperimentManager} from '../../services/experiment.manager';
1511

@@ -22,7 +18,6 @@ import {styles} from './experiment_settings_editor.scss';
2218
export class ExperimentSettingsEditor extends MobxLitElement {
2319
static override styles: CSSResultGroup = [styles];
2420

25-
private readonly analyticsService = core.getService(AnalyticsService);
2621
private readonly experimentEditor = core.getService(ExperimentEditor);
2722
private readonly experimentManager = core.getService(ExperimentManager);
2823

@@ -92,9 +87,16 @@ export class ExperimentSettingsEditor extends MobxLitElement {
9287
this.experimentEditor.updatePermissions({visibility});
9388
};
9489

90+
const isBehaviorEnabled =
91+
this.experimentEditor.experiment.collectBehaviorData;
92+
93+
const updateBehavior = () => {
94+
this.experimentEditor.updateCollectBehaviorData(!isBehaviorEnabled);
95+
};
96+
9597
return html`
9698
<div class="section">
97-
<div class="checkbox-wrapper">
99+
<div class="checkbox-wrapper" style="margin-top: 8px;">
98100
<md-checkbox
99101
touch-target="wrapper"
100102
?checked=${isPublic}
@@ -107,6 +109,19 @@ export class ExperimentSettingsEditor extends MobxLitElement {
107109
manage the experiment dashboard if you share the link with them)
108110
</div>
109111
</div>
112+
<div class="checkbox-wrapper">
113+
<md-checkbox
114+
touch-target="wrapper"
115+
?checked=${isBehaviorEnabled}
116+
?disabled=${!this.experimentEditor.isCreator}
117+
@click=${updateBehavior}
118+
>
119+
</md-checkbox>
120+
<div>
121+
Collect behavior data for bot detection (may incur additional
122+
database costs)
123+
</div>
124+
</div>
110125
</div>
111126
`;
112127
}

frontend/src/components/participant_view/cohort_landing.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ExperimentService} from '../../services/experiment.service';
1010
import {FirebaseService} from '../../services/firebase.service';
1111
import {Pages, RouterService} from '../../services/router.service';
1212
import {ExperimentManager} from '../../services/experiment.manager';
13+
import {BehaviorService} from '../../services/behavior.service';
1314

1415
import {bootParticipantCallable} from '../../shared/callables';
1516

@@ -24,10 +25,26 @@ export class CohortLanding extends MobxLitElement {
2425
private readonly experimentService = core.getService(ExperimentService);
2526
private readonly firebaseService = core.getService(FirebaseService);
2627
private readonly routerService = core.getService(RouterService);
28+
private readonly behaviorService = core.getService(BehaviorService);
2729

2830
@state() isLoading = false;
2931
@state() showResumeDialog = false;
3032
@state() resumeParticipantId: string = '';
33+
private hasLoggedCtaRender = false;
34+
35+
override updated() {
36+
// Log first time the join CTA is actually available to the user
37+
const params = this.routerService.activeRoute.params;
38+
const exp = this.experimentService.experiment;
39+
const isLocked = !!(exp && exp.cohortLockMap[params['cohort']]);
40+
if (!isLocked && !this.hasLoggedCtaRender) {
41+
this.behaviorService.log('cta_render', {
42+
cta: 'join_experiment',
43+
page: 'cohort_landing',
44+
});
45+
this.hasLoggedCtaRender = true;
46+
}
47+
}
3148

3249
override render() {
3350
const isLockedCohort = () => {
@@ -89,6 +106,11 @@ export class CohortLanding extends MobxLitElement {
89106
}
90107

91108
private async joinExperiment() {
109+
// Log CTA click for time-to-first-interaction
110+
this.behaviorService.log('cta_click', {
111+
cta: 'join_experiment',
112+
page: 'cohort_landing',
113+
});
92114
this.isLoading = true;
93115
this.analyticsService.trackButtonClick(ButtonClick.PARTICIPANT_JOIN);
94116

frontend/src/components/stages/chat_interface.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import './chat_message';
99

1010
import {MobxLitElement} from '@adobe/lit-mobx';
1111
import {CSSResultGroup, html, nothing} from 'lit';
12+
import {ref} from 'lit/directives/ref.js';
1213
import {customElement, property, state} from 'lit/decorators.js';
1314

1415
import {core} from '../../core/core';
@@ -17,6 +18,7 @@ import {CohortService} from '../../services/cohort.service';
1718
import {ExperimentService} from '../../services/experiment.service';
1819
import {ParticipantService} from '../../services/participant.service';
1920
import {ParticipantAnswerService} from '../../services/participant.answer';
21+
import {BehaviorService} from '../../services/behavior.service';
2022
import {RouterService} from '../../services/router.service';
2123
import {getHashBasedColor} from '../../shared/utils';
2224

@@ -43,6 +45,7 @@ export class ChatInterface extends MobxLitElement {
4345
private readonly participantAnswerService = core.getService(
4446
ParticipantAnswerService,
4547
);
48+
private readonly behaviorService = core.getService(BehaviorService);
4649
private readonly routerService = core.getService(RouterService);
4750

4851
@property() stage: ChatStageConfig | undefined = undefined;
@@ -66,10 +69,14 @@ export class ChatInterface extends MobxLitElement {
6669
this.startTimer();
6770
}
6871

69-
disconnectedCallback() {
70-
super.disconnectedCallback();
72+
override disconnectedCallback() {
7173
window.removeEventListener('resize', this.updateResponsiveState);
7274
this.clearTimer();
75+
if (this._chatInputUnsub) {
76+
this._chatInputUnsub();
77+
this._chatInputUnsub = null;
78+
}
79+
super.disconnectedCallback();
7380
}
7481

7582
private startTimer() {
@@ -114,6 +121,22 @@ export class ChatInterface extends MobxLitElement {
114121
return null;
115122
}
116123

124+
private _chatInputUnsub: (() => void) | null = null;
125+
126+
private onChatRef = (el: Element | undefined) => {
127+
// Detach previous listener if any
128+
if (this._chatInputUnsub) {
129+
this._chatInputUnsub();
130+
this._chatInputUnsub = null;
131+
}
132+
if (!el) return;
133+
const stageId = this.stage?.id ?? 'unknown';
134+
this._chatInputUnsub = this.behaviorService.attachTextInput(
135+
el,
136+
`chat_stage:${stageId}`,
137+
);
138+
};
139+
117140
private sendUserInput() {
118141
if (!this.stage) return;
119142

@@ -284,6 +307,7 @@ export class ChatInterface extends MobxLitElement {
284307
this.isConversationOver()}
285308
@keyup=${handleKeyUp}
286309
@input=${handleInput}
310+
${ref(this.onChatRef)}
287311
>
288312
</pr-textarea>
289313
<pr-tooltip

0 commit comments

Comments
 (0)