Skip to content

Commit 79fd12f

Browse files
authored
Merge pull request scratchfoundation#64 from cwillisf/fix-telemetry-more
More telemetry fixes (plus project title editing)
2 parents 414a1f0 + 00175f5 commit 79fd12f

File tree

5 files changed

+129
-33
lines changed

5 files changed

+129
-33
lines changed

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
"eslint-plugin-import": "^2.18.0",
4646
"eslint-plugin-react": "^7.14.2",
4747
"intl": "1.2.5",
48+
"lodash.bindall": "^4.4.0",
49+
"lodash.defaultsdeep": "^4.6.1",
4850
"mkdirp": "^0.5.1",
4951
"nets": "^3.2.0",
5052
"react": "16.2.0",

src/main/ScratchDesktopTelemetry.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import {app, ipcMain} from 'electron';
2+
import defaultsDeep from 'lodash.defaultsdeep';
23

34
import TelemetryClient from './telemetry/TelemetryClient';
45

56
const EVENT_TEMPLATE = {
67
version: '3.0.0',
78
projectName: '',
89
language: '',
9-
scriptCount: -1,
10-
spriteCount: -1,
11-
variablesCount: -1,
12-
blocksCount: -1,
13-
costumesCount: -1,
14-
listsCount: -1,
15-
soundsCount: -1
10+
metadata: {
11+
scriptCount: -1,
12+
spriteCount: -1,
13+
variablesCount: -1,
14+
blocksCount: -1,
15+
costumesCount: -1,
16+
listsCount: -1,
17+
soundsCount: -1
18+
}
1619
};
1720

1821
const APP_ID = 'scratch-desktop';
@@ -42,19 +45,44 @@ class ScratchDesktopTelemetry {
4245
}
4346

4447
projectDidLoad (metadata = {}) {
45-
this._telemetryClient.addEvent('project::load', {...EVENT_TEMPLATE, ...metadata});
48+
this._telemetryClient.addEvent('project::load', this._buildMetadata(metadata));
4649
}
4750

4851
projectDidSave (metadata = {}) {
49-
this._telemetryClient.addEvent('project::save', {...EVENT_TEMPLATE, ...metadata});
52+
// Since the save dialog appears on the main process the GUI does not wait for the actual save to complete.
53+
// That means the GUI sends this event before we know the file name used for the save, which is where the new
54+
// project title comes from. Instead, just hold on to this metadata pending a `projectSaveCompleted` event
55+
// from the save code on the main process. If the user cancels the save this data will be cleared.
56+
this._pendingProjectSave = metadata;
57+
}
58+
59+
projectSaveCompleted (newProjectTitle) {
60+
const metadata = this._pendingProjectSave;
61+
this._pendingProjectSave = null;
62+
63+
metadata.projectName = newProjectTitle;
64+
this._telemetryClient.addEvent('project::save', this._buildMetadata(metadata));
65+
}
66+
67+
projectSaveCanceled () {
68+
this._pendingProjectSave = null;
5069
}
5170

5271
projectWasCreated (metadata = {}) {
53-
this._telemetryClient.addEvent('project::create', {...EVENT_TEMPLATE, ...metadata});
72+
this._telemetryClient.addEvent('project::create', this._buildMetadata(metadata));
5473
}
5574

5675
projectWasUploaded (metadata = {}) {
57-
this._telemetryClient.addEvent('project::upload', {...EVENT_TEMPLATE, ...metadata});
76+
this._telemetryClient.addEvent('project::upload', this._buildMetadata(metadata));
77+
}
78+
79+
_buildMetadata (metadata) {
80+
const { projectName, language, ...codeMetadata } = metadata;
81+
return defaultsDeep({
82+
projectName,
83+
language,
84+
metadata: codeMetadata
85+
}, EVENT_TEMPLATE);
5886
}
5987
}
6088

src/main/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,22 @@ const createMainWindow = () => {
6262
if (extName) {
6363
const extNameNoDot = extName.replace(/^\./, '');
6464
const options = {
65+
defaultPath: baseName,
6566
filters: [getFilterForExtension(extNameNoDot)]
6667
};
6768
const userChosenPath = dialog.showSaveDialog(window, options);
6869
if (userChosenPath) {
6970
item.setSavePath(userChosenPath);
71+
const newProjectTitle = path.basename(userChosenPath, extName);
72+
webContents.send('setTitleFromSave', {title: newProjectTitle});
73+
74+
// "setTitleFromSave" will set the project title but GUI has already reported the telemetry event
75+
// using the old title. This call lets the telemetry client know that the save was actually completed
76+
// and the event should be committed to the event queue with this new title.
77+
telemetry.projectSaveCompleted(newProjectTitle);
7078
} else {
7179
item.cancel();
80+
telemetry.projectSaveCanceled();
7281
}
7382
}
7483
});

src/renderer/app.jsx

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {ipcRenderer, shell} from 'electron';
2+
import bindAll from 'lodash.bindall';
3+
import PropTypes from 'prop-types';
24
import React from 'react';
35
import ReactDOM from 'react-dom';
4-
import GUI, {AppStateHOC} from 'scratch-gui';
6+
import {compose} from 'redux';
7+
import GUI, {AppStateHOC, TitledHOC} from 'scratch-gui';
58

69
import ElectronStorageHelper from '../common/ElectronStorageHelper';
710

@@ -23,27 +26,69 @@ appTarget.className = styles.app || 'app'; // TODO
2326
document.body.appendChild(appTarget);
2427

2528
GUI.setAppElement(appTarget);
26-
const WrappedGui = AppStateHOC(GUI);
2729

28-
const onStorageInit = storageInstance => {
29-
storageInstance.addHelper(new ElectronStorageHelper(storageInstance));
30-
// storageInstance.addOfficialScratchWebStores(); // TODO: do we want this?
31-
};
32-
33-
const guiProps = {
34-
onStorageInit,
35-
isScratchDesktop: true,
36-
projectId: defaultProjectId,
37-
showTelemetryModal: (typeof ipcRenderer.sendSync('getTelemetryDidOptIn')) !== 'boolean',
38-
onTelemetryModalOptIn: () => {
39-
ipcRenderer.send('setTelemetryDidOptIn', true);
40-
},
41-
onTelemetryModalOptOut: () => {
42-
ipcRenderer.send('setTelemetryDidOptIn', false);
43-
},
44-
onProjectTelemetryEvent: (event, metadata) => {
45-
ipcRenderer.send(event, metadata);
30+
const ScratchDesktopHOC = function (WrappedComponent) {
31+
class ScratchDesktopComponent extends React.Component {
32+
constructor (props) {
33+
super(props);
34+
bindAll(this, [
35+
'handleProjectTelemetryEvent',
36+
'handleSetTitleFromSave',
37+
'handleStorageInit',
38+
'handleTelemetryModalOptIn',
39+
'handleTelemetryModalOptOut'
40+
]);
41+
}
42+
componentDidMount () {
43+
ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
44+
}
45+
componentWillUnmount () {
46+
ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave);
47+
}
48+
handleProjectTelemetryEvent (event, metadata) {
49+
ipcRenderer.send(event, metadata);
50+
}
51+
handleSetTitleFromSave (event, args) {
52+
this.props.onUpdateProjectTitle(args.title);
53+
}
54+
handleStorageInit (storageInstance) {
55+
storageInstance.addHelper(new ElectronStorageHelper(storageInstance));
56+
}
57+
handleTelemetryModalOptIn () {
58+
ipcRenderer.send('setTelemetryDidOptIn', true);
59+
}
60+
handleTelemetryModalOptOut () {
61+
ipcRenderer.send('setTelemetryDidOptIn', false);
62+
}
63+
render () {
64+
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
65+
return (<WrappedComponent
66+
isScratchDesktop
67+
projectId={defaultProjectId}
68+
showTelemetryModal={shouldShowTelemetryModal}
69+
onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
70+
onStorageInit={this.handleStorageInit}
71+
onTelemetryModalOptIn={this.handleTelemetryModalOptIn}
72+
onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
73+
{...this.props}
74+
/>);
75+
}
4676
}
77+
78+
ScratchDesktopComponent.propTypes = {
79+
onUpdateProjectTitle: PropTypes.func
80+
};
81+
82+
return ScratchDesktopComponent;
4783
};
48-
const wrappedGui = React.createElement(WrappedGui, guiProps);
49-
ReactDOM.render(wrappedGui, appTarget);
84+
85+
// note that redux's 'compose' function is just being used as a general utility to make
86+
// the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's
87+
// ability to compose reducers.
88+
const WrappedGui = compose(
89+
AppStateHOC,
90+
TitledHOC,
91+
ScratchDesktopHOC // must come after `TitledHOC` so it has access to `onUpdateProjectTitle`
92+
)(GUI);
93+
94+
ReactDOM.render(<WrappedGui />, appTarget);

0 commit comments

Comments
 (0)