Quiz analysis with Canvas API

I wanted to perform Item Analysis on quiz results under Canvas LMS.

Without working very hard.

I used a Web Inspector to look at the traffic, found the URL for quiz statistics and started trying to use curl to authenticate in and get the data.

When it didn't work right away, I Googled and found the Canvas REST API documentation. I spent a little time roaming through and found ...

curl -H "Authorization: Bearer REDACTED" https://canvas.instructure.com/api/v1/courses/:course_id/quizzes/:quiz_id/statistics

A single curl request delivers all the stats for a quiz. Absolutely excellent.

To create a .csv of the data, I used PHP, then translated it to Node.js


<?php
// The JavaScript code handles more types of questions - refer to that if you are planning to use this code
$shortopts = "f:q:";

$longopts  = [
    "file:",
    "questions:"
];
$options = getopt($shortopts, $longopts);

$filename = $options['f'];
if (is_file($filename)) {
        $questionList = $options['q'];
        $questions = explode(',', $questionList);
        if (count($questions) < 1) {
                die('No questions');
        }       
} else {
        die('File not found');
}

$keys = [ 'responses', 'answered', 'correct', 'partially_correct', 'incorrect' ];
$assessmentData = json_decode(file_get_contents('stats.json'));
if (json_last_error() === JSON_ERROR_NONE) {
        $quizStatistics = $assessmentData->quiz_statistics[0];
        echo 'question,'.implode(',', $keys).PHP_EOL;
        foreach ($questions as $q) {
                $data = getData($keys, $q, $quizStatistics->question_statistics[$q]);
                echo implode(',', $data).PHP_EOL;
        }
}

function getData($keys, $question,$data) {
        $lineData = [ $question ];
        foreach ($keys as $k) {
                $lineData[] = isset($data->$k) ? $data->$k : 0;
        }
        return $lineData;
}


"use strict";

// Thanks to: https://stackabuse.com/command-line-arguments-in-node-js/
// Thanks to: https://flaviocopes.com/how-to-check-if-file-exists-node/
// Thanks to: https://code-maven.com/reading-a-file-with-nodejs

const fs = require('fs');

const path = process.argv[2];
try {
	if (fs.existsSync(path)) {
		let questionList = process.argv[3];
		let questions = questionList.split(',');
		if (questions.length < 1) {
			throw new Error('No questions');
		}
		fs.readFile(path, 'utf8', function(err,contents) {
				let csvData = createCSVData(contents,questions);
				process.stdout.write(csvData + '\n');
				});	
	} else {
		throw new Error('File not found');
	}
} catch (err) {
	console.error(err);
}

function createCSVData(fileData,questions){
	const keys = [ 'responses', 'answered', 'correct', 'partially_correct', 'incorrect' ];
	let csvData = '';
	let assessmentData = JSON.parse(fileData);
	if (assessmentData !== null && typeof assessmentData !== "undefined") {
		let quizStatistics = assessmentData.quiz_statistics[0];
		csvData = 'question,'+ keys.join(',') + '\n';
		questions.forEach((q) => { 
                                q--; // questions are zero based
				let data = getData(keys, q, quizStatistics.question_statistics[q]);
				csvData += data.join(',') + '\n';
				});
	}
	return csvData;
}

function getData(keys, question, data) {
	let lineData = [ question ];
	keys.forEach(k => {
		let val = 0;
		if (typeof data[k] !== "undefined") {
			val = data[k];
		} else {
			if (typeof data[k + '_student_count'] !== "undefined") {
				val = data[k + '_student_count'];
			}
		}
		lineData.push(val);
	});
	switch (data.question_type) {
		case "essay_question":
			data.point_distribution.forEach(o => {;
				switch (o.score) {
					case data.full_credit:
						lineData[keys.indexOf('correct')+1] = o.count;
						break;
					case 0:
						lineData[keys.indexOf('incorrect')+1] = o.count;
						break;
					default:
						lineData[keys.indexOf('partially_correct')+1] += o.count;
						break;
				}					
			});
			break;
		case "true_false_question":
		case "short_answer_question":
			data.answers.forEach(o => {;
				if (o.correct === true) {
					lineData[keys.indexOf('correct')+1] = o.responses;
				} else {
					lineData[keys.indexOf('incorrect')+1] = o.responses;
				}					
			});
			break;			
		default:
		    	//console.log(question + ". "+data.question_type);
			//console.log(data);
	}
	return lineData;
}

Sample output:

question,responses,answered,correct,partially_correct,incorrect
2,11,11,3,4,6

Ref: https://canvas.instructure.com/doc/api/
Ref: https://canvas.instructure.com/doc/api/file.oauth.html#manual-token-generation - get a user auth token