As part of Google Docs, Sheets and Slides the user interface includes access to the file’s version history (File > Version history > See version history). Google Forms doesn’t have this feature yet but some version history data is available using Google Drive API Revisions resource or Google Drive Activity API.
As I find the revision history in Docs, Sheets and Slides incredibly useful to audit changes and rollback to earlier versions I thought it would be worth developing a Google Forms add-on which gave users a way to see if changes have been made to a Google Form, create back-up versions, and access previous copies. The result is Forms History which you can get from the Google Workspace Marketplace.
Having spent quite a bit of time working with the Google Drive API Revisions resource in this post I thought it would be useful to share some of the lessons and solutions I’ve picked up along the way. For this I’ll be sharing code snippets for interacting with the Revisions
resource with Google Apps Script, but the solutions discussed could easily be applied to your programming language of choice.
Basic function and setup for revisions.list
Below is the Forms History function I use to get the revision list for a Google Form. To use this the Advanced Drive Service is being used and the reference documents provide instructions on setup. To return a revisions.list
you need to include one of the listed scopes.
Note: The Advanced Drive Service currently uses v2 not the latest v3. In the case of revisions.list
v2 actually returns more metadata in the Revisions
resource and you can use the API Explorer to see what data is returned.
/**
* Queries the Drive Revisions endpoint for the Form edit history
*
* @return {Object} An object array of form edits.
*/
function getHistory(){
try {
const form = FormApp.getActiveForm();
// Based on a pattern by Tanaike
// https://stackoverflow.com/a/70289664/1027723
let pageToken = null;
let revisionList = [];
do {
let response = Drive.Revisions.list(form.getId(), {
"pageToken": pageToken,
"maxResults": 1000
});
if (response.items.length > 0) revisionList = [...revisionList, ...response.items];
pageToken = response.nextPageToken;
} while (pageToken);
return revisionList;
} catch (e) {
console.error('getHistory() yielded an error: ' + e.stack);
}
}
Merging of edits and last modifying user
Google make it clear in the documentation that the Revisions
resource might not return every version of a file:
Each time the content changes, Drive creates a new revision history entry for that file. However, these editor file revisions may be merged together, so the API response might not show all changes to a file.
Changes and revisions overview
In the case of the Forms History add-on merging small edits into a single revision was actually a useful feature and it meant the user wasn’t overwhelmed with a long list of edits when there had been a minor change. The issue however is when multiple editors are making changes within a ~20 minute timeframe it appears the first editor is recorded as the last modifying user even if they didn’t make the last change.
To solve this Forms History uses Google Drive Files.get
which includes the actual lastModifyingUser
in the Files
resource response and overwrites this in the last revision item:
/**
* Queries the Drive Revisions endpoint for the Form edit history
*
* @return {Object} An object array of form edits.
*/
function getHistory(){
try {
const form = FormApp.getActiveForm();
// Based on a pattern by Tanaike
// https://stackoverflow.com/a/70289664/1027723
let pageToken = null;
let revisionList = [];
do {
let response = Drive.Revisions.list(form.getId(), {
"pageToken": pageToken,
"maxResults": 1000
});
if (response.items.length > 0) revisionList = [...revisionList, ...response.items];
pageToken = response.nextPageToken;
} while (pageToken);
// get the actual lastModifyingUser (revisions endpoint isn't always correct)
const file = Drive.Files.get(form.getId());
revisionList[revisionList.length - 1].lastModifyingUser = file.lastModifyingUser;
return revisionList;
} catch (e) {
console.error('getHistory() yielded an error: ' + e.stack);
}
}
Missing revisionId
as a result of merged edits
A feature of Forms History is the ability for users to create a back-up copy of their form which is included as a link in the revision history. An issue encountered related to the merging of edits was a previously returned revisionId
would be dropped from Revisions
resource if there was an minor edit. This meant links to specific revisions were missing.
In the case of the Forms History add-on the solution was when a user made a copy of the form the Revisions
resource for the copied revision is stored in Document Properties:
// store revision locally (Google will drop revisions from revision.list if they are similar)
PropertiesService.getDocumentProperties().setProperty('REV_'+revision.id, JSON.stringify(revision));
Revisions stored in Document Properties are then merged back into the Revisions
resource:
function getRenderedHistory(){
try {
const items = getHistory();
const prop = PropertiesService.getDocumentProperties().getProperties();
// Add any locally stored revisions to the revision list
// (Google will drop revisions from revision.list if they are similar)
const revs = Object.keys(prop)
.filter(v => v.startsWith('REV_'));
revs.map(r => {
if (!items.some(i => { if (r === `REV_${i.id}`) return true})){
items.push(JSON.parse(prop[r]));
};
});
// sort array in case new items added from local storage
items.sort((a, b) => parseInt(b.id) > parseInt(a.id) ? 1 : -1);
// ...
} catch (e) {
console.error('getRenderedHistory() yielded an error: ' + e.stack);
return e
}
}
Summary
Hopefully this post has proved useful if you are considering using the Google Drive API Revisions resource. Whilst I’ve focused on usage with Google Forms Revisions can be used with other file types stored in Google Drive. Depending on your use case you may want to also consider other Drive APIs like the Google Drive Activity API or for Google Form specific projects the Forms REST API, which I’ve previously shared developer notes.
Featured image credit: Alice Keeler (alicekeeler.com)