키스트로크 로깅
Potato는 어노테이터가 입력한 문자를 전혀 기록하지 않으면서도 자유 텍스트 답변 뒤에 놓인 멈춤, 버스트, 수정, 붙여넣기를 기록할 수 있습니다.
Potato는 자유 텍스트 답변을 기록하지 않으면서 그 답변이 어떻게 만들어졌는지를 기록할 수 있습니다. 모든 이벤트는 타임스탬프, 입력 유형, 키 부류, 길이 변화를 담지만, 그중 어느 것도 입력된 문자를 담지는 않습니다. Potato는 이 스트림에서 약 40개의 요약 특징을 계산해 어노테이션과 함께 저장합니다.
목적은 답을 하나씩 궁리해 가며 멈춤과 재고를 거쳐 작성한 답변을, 다른 창에서 다시 옮겨 친 답변이나 챗봇에서 그대로 붙여넣은 답변과 구분하는 것입니다. 완성된 답변만 읽으면 서로 비슷해 보입니다. 로그는 그렇지 않습니다.
키스트로크 로깅에는 Potato 2.7.2 이상이 필요하며 기본값은 비활성입니다. keystroke_logging.enabled는 여러분이 설정하기 전까지 false이므로, 업그레이드만으로 누군가의 기록이 시작되는 일은 없습니다. 이 데이터 위에 세워진 규칙은 작성 과정 탐지를 참고하세요. 사람 참가자에게 적용하기 전에 키스트로크 로깅 윤리를 읽어 보세요.
빠른 시작
keystroke_logging:
enabled: true최소 설정은 이것이 전부입니다. 프로젝트의 모든 자유 텍스트 필드가 내용을 남기지 않는 이벤트 스트림과 요약, 그리고 탐지 플래그를 만들어 내기 시작합니다.
실행해 볼 수 있는 예제가 Potato와 함께 제공됩니다.
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000경고:
enabled의 기본값은false입니다. Potato를 업그레이드한다고 해서 어노테이터의 기록이 조용히 시작되지는 않습니다.
무엇이 수집되는가
각 이벤트는 타임스탬프, 입력 유형, 키 부류, 캐럿 위치, 필드 길이의 변화를 기록합니다.
{t_ms: 1240, input_type: "insertText", key_class: "letter", pos: 41, delta: +1}
{t_ms: 1310, input_type: "insertText", key_class: "letter", pos: 42, delta: +1}
{t_ms: 3980, input_type: "deleteContentBackward", key_class: "bksp", pos: 42, delta: -1}
{t_ms: 9120, input_type: "insertFromPaste", key_class: "unknown",pos: 43, delta: +287,
meta: {paste_source: "external", paste_hash: "sekqf3"}}
의도적으로 수집하지 않는 것
| 수집하지 않는 것 | 이유 |
|---|---|
| 입력된 문자 | 스트림은 텍스트가 아니라 과정을 재구성합니다 |
| 붙여넣은 텍스트 | 길이, 출처 레이블, 솔트를 붙인 해시만 남깁니다 |
| 중간 초안 | 길이 변화량만으로는 복원할 수 없습니다 |
| 비밀번호 필드의 모든 것 | getFieldIdentity가 type="password"를 아예 거부합니다 |
| 클립보드 내용 일반 | 붙여넣는 시점에 분류하려고 읽은 뒤 버립니다 |
키 부류
키 자체는 저장되지 않고 어느 부류에 속하는지만 저장됩니다.
letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown
입력 유형
Potato의 주 신호는 keydown이 아니라 beforeinput에서 읽는 InputEvent.inputType입니다. 이것이 핵심적인 기술 선택입니다. 붙여넣기, 드래그 앤 드롭, IME 조합, 받아쓰기, 자동 완성, 실행 취소는 모두 keydown을 전혀 발생시키지 않고 필드를 변경하므로, keydown만 보는 로거는 이 기능이 탐지하려는 바로 그 경우들에 눈이 멀어 있습니다.
수집되는 입력 유형은 insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo이며, 여기에 합성 이벤트인 focus, blur, keydown이 더해집니다.
keydown과 keyup도 여전히 듣지만, 물리적인 키 입력을 세고 누름 시간을 재는 데에만 씁니다. 실제로 나타난 문자와 실제로 눌린 키 사이의 차이가 수집되는 것 중 가장 강한 단일 신호입니다. 아래의 silent_insert_ratio를 참고하세요.
어떤 필드가 계측되는가
기본적으로 모든 자유 텍스트 필드가 대상입니다. text 스키마, radio와 multiselect 안의 자유 응답 입력란, 그리고 text_edit, pairwise, trajectory_eval 등의 스키마에 있는 근거나 메모 텍스트 영역이 여기에 해당합니다.
필드는 Potato가 이미 모든 어노테이션 입력 요소에 붙여 두는 schema와 label_name 속성으로 식별하며, 없으면 name 속성을 :::로 나누는 방식으로 대체합니다.
두 목록 중 하나로 범위를 좁힐 수 있습니다.
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylist또는 커스텀 HTML에서 요소 하나만 제외할 수도 있습니다.
<textarea data-keystroke-logging="off" ...></textarea>설정 레퍼런스
keystroke_logging:
enabled: false # master switch
fidelity: events # off | summary | events
include_schemas: [] # empty = every free-text field
exclude_schemas: []
store_events: true # persist raw streams (needs fidelity: events)
classify_paste_source: true # label pastes self/instance_text/ai_suggestion/external
idle_session_ms: 30000 # close a session after this much inactivity
flush_interval_ms: 5000 # how often the browser posts completed sessions
pause_thresholds_ms: [500, 1000, 2000, 5000, 10000]
disclose_to_annotators: true # show a recording notice
detection:
enabled: true
calibrate: false # use project-fitted thresholds
on_external_insert: flag # allow | warn | block | flag
thresholds: {} # per-rule overrides| 키 | 기본값 | 의미 |
|---|---|---|
enabled | false | 마스터 스위치입니다. false이면 아무것도 수집하지 않습니다. |
fidelity | events | off는 비활성화, summary는 특징만 계산하고 스트림은 저장하지 않음, events는 둘 다 저장합니다. |
include_schemas | [] | 허용할 스키마 이름 목록입니다. 비어 있으면 전체를 뜻합니다. |
exclude_schemas | [] | 차단 목록이며, 허용 목록 뒤에 적용됩니다. |
store_events | true | 원시 스트림을 보존합니다. fidelity: events가 아니면 무시됩니다. |
classify_paste_source | true | 붙여넣기를 지문, AI 제안, 필드의 기존 내용과 대조합니다. |
idle_session_ms | 30000 | 세션을 닫고 전송하기까지의 무활동 시간입니다. |
flush_interval_ms | 5000 | 브라우저의 전송 주기입니다. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | 각 기준마다 멈춤 횟수를 보고합니다. |
disclose_to_annotators | true | 기록 중임을 알리는 고지를 표시합니다. 끄면 경고가 기록됩니다. |
탐지 관련 키는 작성 과정 탐지에서 다룹니다.
fidelity 고르기
| Fidelity | 스트림 저장 | 나중에 새 지표를 다시 계산할 수 있나 | 사용 시점 |
|---|---|---|---|
off | — | — | 이 프로젝트에서 기능을 끕니다 |
summary | 아니요 | 아니요 | 필요한 특징이 무엇인지 확실하거나, 윤리 승인이 스트림 보관까지 다루지 않는 경우 |
events | 예 | 예 | 기본값입니다. 키 입력당 약 2바이트 |
events가 권장 설정입니다. 500단어 응답이 약 5KB이고, 데이터 수집이 끝난 뒤에 떠오른 지표도 여전히 계산할 수 있다는 뜻입니다.
요약 특징
(사용자, 인스턴스, 필드)마다 요약이 하나씩 만들어집니다. 특징 계열은 Crossley et al. (2024)를 따릅니다. 연구적 근거를 참고하세요.
분량과 결과물 대 과정
| 필드 | 의미 |
|---|---|
keystrokes | 텍스트를 만들어 낸 물리적 keydown 수 |
final_chars | 세션 종료 시점의 필드 길이 |
chars_typed / chars_inserted | 타이핑으로 삽입된 문자 수 / 모든 방식으로 삽입된 문자 수 |
chars_deleted | 삭제된 문자 수 |
chars_per_keystroke | 약 1.1을 넘으면 키 입력 없이 텍스트가 들어왔다는 뜻입니다 |
active_ms / wall_ms | 자리를 비운 시간을 제외한 / 포함한 필드 체류 시간 |
리듬
| 필드 | 의미 |
|---|---|
iki_median_ms, iki_mean_ms | 키 간격의 중심 경향 |
iki_p10/p25/p75/p90_ms | 간격 분포의 형태 |
iki_log_sd, iki_log_cv | 로그 척도상의 산포도입니다. 값이 낮으면 기계적이라는 뜻이고, 이는 옮겨 적기를 뜻합니다. |
로그 척도를 쓰는 이유는 키 간격 분포가 오른쪽으로 크게 치우쳐 있기 때문입니다. 30초를 넘는 간격은 이 통계에서 제외해, 커피 한 잔 마신 시간이 통계를 지배하지 못하게 합니다.
멈춤
| 필드 | 의미 |
|---|---|
pause_counts | 설정된 각 기준에서의 횟수 |
pause_total_ms | 멈춤에 쓰인 총 시간 |
pre_word_pause_mean_ms | 단어를 시작하기 전 평균 멈춤 |
pre_sentence_pause_mean_ms | 문장 부호 뒤의 평균 멈춤 |
intraword_iki_median_ms | 단어 내부 간격의 중앙값으로, 타자 숙련도의 대리 지표 |
버스트
| 필드 | 의미 |
|---|---|
bursts, burst_mean_chars, burst_max_chars | 연속 산출 통계 |
p_bursts | 멈춤으로 끝난 버스트 |
r_bursts | 수정으로 끝난 버스트 |
수정
| 필드 | 의미 |
|---|---|
backspaces, deletes, undo_events | 삭제 행동 |
non_terminal_edits | 텍스트 끝보다 앞에서 이루어진 편집으로, 글쓴이가 되돌아가 고쳤다는 뜻 |
caret_jumps | 인접하지 않은 위치로의 캐럿 이동 |
revision_ratio | chars_deleted / chars_typed |
외부 삽입
| 필드 | 의미 |
|---|---|
paste_events, pasted_chars, largest_paste_chars | 붙여넣기 분량 |
pasted_fraction | 최종 텍스트 중 붙여넣기가 차지하는 비율 |
drop_events | 드래그 앤 드롭 삽입 |
silent_insert_chars / silent_insert_ratio | 대응하는 키 입력이 없는 문자 |
external_insert_chars / external_insert_ratio | 위와 같되 자기 인용과 지문 인용은 제외 |
paste_sources, paste_chars_by_source | 출처 레이블별 횟수와 문자 수 |
탐지에는 external_insert_ratio를 쓰세요. silent_insert_ratio는 정당한 경우까지 포함해 모든 조용한 삽입을 셉니다.
주의
| 필드 | 의미 |
|---|---|
blur_events, blur_total_ms | 페이지를 벗어나 있던 시간 |
max_blur_before_insert_ms | 큰 삽입 직전의 가장 긴 이탈 시간 |
first_keystroke_latency_ms | 첫 글자를 치기 전의 생각 시간 |
무결성
| 필드 | 의미 |
|---|---|
untrusted_events | InputEvent.isTrusted === false, 즉 스크립트나 자동화로 만들어진 입력 |
composition_events | IME 조합 |
virtual_keyboard | 모바일 또는 소프트 키보드가 감지됨 |
데이터가 저장되는 곳
목적이 다른 두 곳에 저장됩니다.
원시 스트림은 SQLite로
<task_dir>/project.sqlite의 typing_sessions 테이블에 세션당 한 행씩, 메모와 코드북이 쓰는 것과 같은 영속성 계층을 거쳐 저장됩니다.
조회에 쓰는 요약 열은 전체 JSON 요약과 zlib으로 압축된 이벤트 블롭 옆에 역정규화되어 함께 저장됩니다.
sqlite3 <task_dir>/project.sqlite "
SELECT user_id, schema_name, keystrokes, final_chars,
pasted_fraction, silent_insert_ratio, iki_log_cv,
json_extract(flags,'\$.level') AS level
FROM typing_sessions;"스트림은 키 입력마다 한 행이 아니라 세션마다 압축된 블롭 하나로 저장됩니다. 어차피 항상 통째로만 읽어 들이고, 이벤트당 약 2바이트인 상황에서 키 입력당 한 행짜리 스키마는 질의상 이점 없이 프로젝트 파일에 수천만 행을 쌓게 됩니다.
단계 페이지
훈련 단계와 사전·사후 설문의 자유 텍스트 답변도 수집됩니다. 이 페이지들에는 인스턴스 id가 없으므로, 해당 세션은 나머지 행동 데이터 시스템이 이미 쓰고 있는 __phase_page__ 센티널 아래로 모이고 대신 phase와 page 열로 식별됩니다.
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;보정 예제가 작동하는 이유가 이것입니다. 훈련 단계에서 지문을 그대로 따라 치는 과제를 주면 옮겨 적기 예시가 생기고, 이는 phase만으로 평소의 작성 답변과 구분할 수 있습니다.
요약은 user_state.json으로
압축된 스케치는 <output_annotation_dir>/<user>/user_state.json의 instance_id_to_behavioral_data.<instance>.typing_summaries 아래에 "{schema}:::{label}" 키로 미러링되어, 어노테이션과 함께 관리자 대시보드와 내보내기로 따라갑니다.
원시 스트림은 의도적으로 그곳에 들어가지 않습니다. 그 파일은 어노테이션을 저장할 때마다 전부 다시 직렬화되어 원자적으로 덮어 쓰이는데, 긴 응답이면 이벤트가 수천 개입니다.
내보내기
두 내보내기 모두 옵트인이므로, 행동 데이터가 실수로 데이터셋 배포에 포함되는 일은 없습니다.
어노테이션과 함께 나가는 요약 특징
export_include_typing_dynamics: trueannotations.csv 옆에 typing_dynamics.csv(또는 .tsv)를 만들며, (사용자, 인스턴스, 필드)마다 한 행씩 요약 특징과 탐지 결과를 담습니다.
원시 스트림
python -m potato.export.cli <config.yaml> --format keystrokeskeystroke_sessions.parquet과 keystroke_events.parquet을 쓰며, pyarrow가 설치되어 있지 않으면 JSONL로 대체합니다. 내보내기 도구 전반은 Parquet 내보내기를 참고하세요.
import pandas as pd
events = pd.read_parquet("keystroke_events.parquet")
# Inter-key intervals for one session
s = events[events.session_id == events.session_id.iloc[0]].sort_values("t_ms")
iki = s.t_ms.diff().dropna()
print(iki.median(), iki.std())
# Every externally-sourced paste in the project
print(events[events.paste_source == "external"])API 엔드포인트
| 메서드 | 라우트 | 용도 |
|---|---|---|
POST | /api/track_typing | 브라우저에서 완료된 세션을 받습니다 |
GET | /api/typing_summary/<instance_id> | 현재 사용자의 인스턴스 하나에 대한 요약 |
GET | /admin/api/writing_process | 어노테이터별 집계(관리자 키 필요) |
세션 요약은 서버에서 계산합니다. 브라우저가 계산된 요약을 보내는 일이 없으므로 수정된 클라이언트로 수치를 위조할 수 없고, 나중에 추가된 지표도 저장된 스트림에서 다시 계산할 수 있습니다.
세션이 동작하는 방식
세션은 필드가 포커스를 받을 때 시작되고, 포커스를 잃거나, 다른 인스턴스로 이동하거나, idle_session_ms만큼 활동이 없거나, 페이지가 언로드될 때 중 가장 먼저 오는 시점에 끝납니다. 완료된 세션은 flush_interval_ms마다 전송되고, 언로드 시에는 navigator.sendBeacon으로 전송되어 진행 중이던 세션이 유실되지 않습니다.
같은 필드에서 발생한 여러 세션은 요약이 사용자 상태에 기록되기 전에 병합되므로, 필드를 떠났다 돌아온 것은 의심스러울 만큼 짧은 여러 응답이 아니라 하나의 응답으로 읽힙니다. 횟수와 시간은 더해집니다. 분포 통계는 키 입력 수로 가중한 근삿값이므로, 정확한 통합 분포가 필요하면 원시 스트림을 쓰세요.
문제 해결
아무 데이터도 기록되지 않음
keystroke_logging.enabled: true인지, 그리고 fidelity가 off가 아닌지 확인하세요. 브라우저 콘솔에서 window.keystrokeTracker가 존재하고 isInitialized === true여야 합니다. undefined라면 설정이 템플릿까지 전달되지 않은 것입니다.
트래커는 있는데 세션이 나타나지 않음
필드 식별을 확인하세요.
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull이면 그 요소에 schema나 label_name 속성이 없고 :::로 구분된 name도 없거나, 설정에서 제외된 것입니다.
silent_insertion이 모바일 어노테이터마다 걸림
virtual_keyboard가 true이면 이 규칙은 억제되므로 그런 일이 있어서는 안 됩니다. 탐지가 잘못 동작한다면 클라이언트가 그 플래그를 설정했는지 확인하세요. 오탐 표를 참고하세요.
project.sqlite가 커짐
키 입력당 약 2바이트입니다. fidelity: summary로 설정해 특징만 남기고 스트림을 버리거나, typing_store.delete_for_user()로 참가자 한 명의 데이터를 지우세요.
자동화 테스트에서 수치가 이상함
브라우저 자동화는 간격이 거의 0인 상태로 입력하므로 implausible_speed에 실제로 걸립니다. 버그가 아니라 플래그가 제대로 동작하는 것입니다.
더 읽어보기
- 작성 과정 탐지 - 여섯 개의 규칙과 세 단계의 탐지
- 키스트로크 로깅 윤리 - 동의, IRB, 보관, 참가자 권리
- 행동 추적 - 이 기능이 속한 더 넓은 상호작용 추적 시스템
- 품질 관리 - 주의 확인과 골드 스탠다드
- 관리자 대시보드 - 작성 과정 패널이 있는 곳
구현 세부 사항은 원본 문서를 참고하세요.