Commit fedfd84c authored by Kelvin Jiang's avatar Kelvin Jiang Committed by Commit Bot

[Extensions activity log] Add option to export activity log history JSON

This change adds the ability to export the activity log history as JSON
through an option in the 3 dots dropdown menu. The exported data is a
list of extension activities as returned by GetExtensionActivities from
the chrome.activityLogPrivate API (therefore the data might be different
than what's seen in the UI itself) but sorted by timestamp in ascending
order.

Screenshot: https://imgur.com/a/YNpyFCd

Bug: 832354
Change-Id: If891931bfa042e3f1eadecc6f6384198c4a2c6b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1602074
Commit-Queue: Kelvin Jiang <kelvinjiang@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#660693}
parent 1f5a68ed
...@@ -178,6 +178,9 @@ ...@@ -178,6 +178,9 @@
<message name="IDS_EXTENSIONS_ACTIVITY_LOG_COLLAPSE_ALL" desc="The label of the button to collapse all activity log items."> <message name="IDS_EXTENSIONS_ACTIVITY_LOG_COLLAPSE_ALL" desc="The label of the button to collapse all activity log items.">
Collapse all Collapse all
</message> </message>
<message name="IDS_EXTENSIONS_ACTIVITY_LOG_EXPORT_HISTORY" desc="The label of the button to export the activity log history as JSON.">
Export Activities
</message>
<message name="IDS_EXTENSIONS_ITEM_ID" desc="The text for the label next to the extension id."> <message name="IDS_EXTENSIONS_ITEM_ID" desc="The text for the label next to the extension id.">
&lt;span&gt;ID: &lt;/span&gt;<ph name="EXTENSION_ID">$1<ex>cfhdojbkjhnklbpkdaibdccddilifddb</ex></ph> &lt;span&gt;ID: &lt;/span&gt;<ph name="EXTENSION_ID">$1<ex>cfhdojbkjhnklbpkdaibdccddilifddb</ex></ph>
</message> </message>
......
...@@ -72,6 +72,10 @@ ...@@ -72,6 +72,10 @@
on-click="onCollapseAllClick_"> on-click="onCollapseAllClick_">
$i18n{activityLogCollapseAll} $i18n{activityLogCollapseAll}
</button> </button>
<button id="export-button" class="dropdown-item"
on-click="onExportClick_">
$i18n{activityLogExportHistory}
</button>
</cr-action-menu> </cr-action-menu>
</div> </div>
<div id="loading-activities" class="activity-message" <div id="loading-activities" class="activity-message"
......
...@@ -45,6 +45,12 @@ cr.define('extensions', function() { ...@@ -45,6 +45,12 @@ cr.define('extensions', function() {
* @return {!Promise<void>} * @return {!Promise<void>}
*/ */
deleteActivitiesFromExtension(extensionId) {} deleteActivitiesFromExtension(extensionId) {}
/**
* @param {string} rawActivityData
* @param {string} fileName
*/
downloadActivities(rawActivityData, fileName) {}
} }
/** /**
...@@ -165,7 +171,7 @@ cr.define('extensions', function() { ...@@ -165,7 +171,7 @@ cr.define('extensions', function() {
* @return {!Array<!extensions.ActivityGroup>} * @return {!Array<!extensions.ActivityGroup>}
*/ */
function sortActivitiesByCallCount(groupedActivities) { function sortActivitiesByCallCount(groupedActivities) {
return Array.from(groupedActivities.values()).sort(function(a, b) { return Array.from(groupedActivities.values()).sort((a, b) => {
if (a.count != b.count) { if (a.count != b.count) {
return b.count - a.count; return b.count - a.count;
} }
...@@ -225,6 +231,14 @@ cr.define('extensions', function() { ...@@ -225,6 +231,14 @@ cr.define('extensions', function() {
*/ */
dataFetchedResolver_: null, dataFetchedResolver_: null,
/**
* The stringified API response from the activityLogPrivate API with
* individual activities sorted in ascending order by timestamp; used for
* exporting the activity log.
* @private {string}
*/
rawActivities_: '',
/** /**
* Expose only the promise of dataFetchedResolver_. * Expose only the promise of dataFetchedResolver_.
* @return {!Promise<void>} * @return {!Promise<void>}
...@@ -302,6 +316,12 @@ cr.define('extensions', function() { ...@@ -302,6 +316,12 @@ cr.define('extensions', function() {
this.expandItems_(false); this.expandItems_(false);
}, },
/** @private */
onExportClick_: function() {
const fileName = `exported_activity_log_${this.extensionId}.json`;
this.delegate.downloadActivities(this.rawActivities_, fileName);
},
/** /**
* @private * @private
* @param {!CustomEvent<!Array<string>>} e * @param {!CustomEvent<!Array<string>>} e
...@@ -324,6 +344,12 @@ cr.define('extensions', function() { ...@@ -324,6 +344,12 @@ cr.define('extensions', function() {
*/ */
processActivities_: function(activityData) { processActivities_: function(activityData) {
this.pageState_ = ActivityLogPageState.LOADED; this.pageState_ = ActivityLogPageState.LOADED;
// Sort |activityData| in ascending order based on the activity's
// timestamp; Used for |this.encodedRawActivities|.
activityData.sort((a, b) => a.time - b.time);
this.rawActivities_ = JSON.stringify(activityData);
this.activityData_ = this.activityData_ =
sortActivitiesByCallCount(groupActivities(activityData)); sortActivitiesByCallCount(groupActivities(activityData));
if (!this.dataFetchedResolver_.isFulfilled) { if (!this.dataFetchedResolver_.isFulfilled) {
......
...@@ -418,6 +418,16 @@ cr.define('extensions', function() { ...@@ -418,6 +418,16 @@ cr.define('extensions', function() {
getOnExtensionActivity() { getOnExtensionActivity() {
return chrome.activityLogPrivate.onExtensionActivity; return chrome.activityLogPrivate.onExtensionActivity;
} }
/** @override */
downloadActivities(rawActivityData, fileName) {
const blob = new Blob([rawActivityData], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
}
} }
cr.addSingletonGetter(Service); cr.addSingletonGetter(Service);
......
...@@ -205,6 +205,7 @@ content::WebUIDataSource* CreateMdExtensionsSource(Profile* profile, ...@@ -205,6 +205,7 @@ content::WebUIDataSource* CreateMdExtensionsSource(Profile* profile,
IDS_EXTENSIONS_ACTIVITY_LOG_MORE_ACTIONS_LABEL}, IDS_EXTENSIONS_ACTIVITY_LOG_MORE_ACTIONS_LABEL},
{"activityLogExpandAll", IDS_EXTENSIONS_ACTIVITY_LOG_EXPAND_ALL}, {"activityLogExpandAll", IDS_EXTENSIONS_ACTIVITY_LOG_EXPAND_ALL},
{"activityLogCollapseAll", IDS_EXTENSIONS_ACTIVITY_LOG_COLLAPSE_ALL}, {"activityLogCollapseAll", IDS_EXTENSIONS_ACTIVITY_LOG_COLLAPSE_ALL},
{"activityLogExportHistory", IDS_EXTENSIONS_ACTIVITY_LOG_EXPORT_HISTORY},
{"appIcon", IDS_EXTENSIONS_APP_ICON}, {"appIcon", IDS_EXTENSIONS_APP_ICON},
{"extensionIcon", IDS_EXTENSIONS_EXTENSION_ICON}, {"extensionIcon", IDS_EXTENSIONS_EXTENSION_ICON},
{"extensionA11yAssociation", IDS_EXTENSIONS_EXTENSION_A11Y_ASSOCIATION}, {"extensionA11yAssociation", IDS_EXTENSIONS_EXTENSION_A11Y_ASSOCIATION},
......
...@@ -56,6 +56,11 @@ suite('ExtensionsActivityLogHistoryTest', function() { ...@@ -56,6 +56,11 @@ suite('ExtensionsActivityLogHistoryTest', function() {
] ]
}; };
// The first two activities of |testActivities|,
const testExportActivities = {
activities: testActivities.activities.slice(0, 2),
};
// Sample activities representing content script invocations. Activities with // Sample activities representing content script invocations. Activities with
// missing args will not be processed. // missing args will not be processed.
const testContentScriptActivities = { const testContentScriptActivities = {
...@@ -318,6 +323,32 @@ suite('ExtensionsActivityLogHistoryTest', function() { ...@@ -318,6 +323,32 @@ suite('ExtensionsActivityLogHistoryTest', function() {
}); });
}); });
test('export activities', async function() {
// |testExportActivities| stringified and sorted by timestamp.
const expectedRawActivityData =
'[{"activityId":"309","activityType":"dom_access","apiCall":"Storage.' +
'getItem","args":"null","count":35,"extensionId":"aaaaaaaaaaaaaaaaaaa' +
'aaaaaaaaaaaaa","other":{"domVerb":"method"},"pageTitle":"Test Extens' +
'ion","pageUrl":"chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/' +
'index.html","time":1541203131994.837},{"activityId":"299","activityT' +
'ype":"api_call","apiCall":"i18n.getUILanguage","args":"null","count"' +
':10,"extensionId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","time":15412031' +
'32002.664}]';
proxyDelegate.testActivities = testExportActivities;
await setupActivityLogHistory();
Polymer.dom.flush();
activityLogHistory.$$('#more-actions').click();
activityLogHistory.$$('#export-button').click();
const [actualRawActivityData, actualFileName] =
await proxyDelegate.whenCalled('downloadActivities');
expectEquals(expectedRawActivityData, actualRawActivityData);
expectEquals(`exported_activity_log_${EXTENSION_ID}.json`, actualFileName);
});
test( test(
'clicking on the delete button for an activity row deletes that row', 'clicking on the delete button for an activity row deletes that row',
function() { function() {
......
...@@ -10,6 +10,7 @@ cr.define('extensions', function() { ...@@ -10,6 +10,7 @@ cr.define('extensions', function() {
'addRuntimeHostPermission', 'addRuntimeHostPermission',
'deleteActivitiesById', 'deleteActivitiesById',
'deleteActivitiesFromExtension', 'deleteActivitiesFromExtension',
'downloadActivities',
'getExtensionActivityLog', 'getExtensionActivityLog',
'getExtensionsInfo', 'getExtensionsInfo',
'getExtensionSize', 'getExtensionSize',
...@@ -42,7 +43,7 @@ cr.define('extensions', function() { ...@@ -42,7 +43,7 @@ cr.define('extensions', function() {
this.forceReloadItemError_ = false; this.forceReloadItemError_ = false;
/** @type {!chrome.activityLogPrivate.ActivityResultSet|undefined} */ /** @type {!chrome.activityLogPrivate.ActivityResultSet|undefined} */
this.testActivities = undefined; this.testActivities;
} }
/** /**
...@@ -211,6 +212,11 @@ cr.define('extensions', function() { ...@@ -211,6 +212,11 @@ cr.define('extensions', function() {
getOnExtensionActivity() { getOnExtensionActivity() {
return this.extensionActivityTarget; return this.extensionActivityTarget;
} }
/** @override */
downloadActivities(rawActivityData, fileName) {
this.methodCalled('downloadActivities', [rawActivityData, fileName]);
}
} }
return { return {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment