mirror of https://github.com/artf/grapesjs.git
committed by
GitHub
10 changed files with 264 additions and 5 deletions
@ -0,0 +1,34 @@ |
|||||
|
--- |
||||
|
title: GrapesJS Telemetry |
||||
|
--- |
||||
|
|
||||
|
# GrapesJS Telemetry |
||||
|
|
||||
|
We collect and use data to improve GrapesJS. This page explains what data we collect and how we use it. |
||||
|
|
||||
|
## What data we collect |
||||
|
|
||||
|
We collect the following data: |
||||
|
|
||||
|
- **domain**: The domain of the website where GrapesJS is used. |
||||
|
- **version**: The version of GrapesJS used. |
||||
|
- **timestamp**: The time when the editor is loaded. |
||||
|
|
||||
|
## How we use data |
||||
|
|
||||
|
We use data to: |
||||
|
|
||||
|
- **Improve GrapesJS**: We use data to improve GrapesJS. For example, we use data to identify bugs and fix them. |
||||
|
- **Analyze usage**: We use data to analyze how GrapesJS is used. For example, we use data to understand which features are used most often. |
||||
|
- **Provide support**: We use data to provide support to users. For example, we use data to understand how users interact with GrapesJS. |
||||
|
|
||||
|
## How to opt-out |
||||
|
|
||||
|
You can opt-out of data collection by setting the `telemetry` option to `false` when initializing GrapesJS: |
||||
|
|
||||
|
```js |
||||
|
const editor = grapesjs.init({ |
||||
|
// ... |
||||
|
telemetry: false, |
||||
|
}); |
||||
|
``` |
||||
@ -0,0 +1,3 @@ |
|||||
|
export function getHostName() { |
||||
|
return window.location.hostname; |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
import grapesjs from '../../../src'; |
||||
|
import { EditorConfig } from '../../../src/editor/config/config'; |
||||
|
import { fixJsDom, fixJsDomIframe, waitEditorEvent } from '../../common'; |
||||
|
|
||||
|
import * as hostUtil from '../../../src/utils/host-name'; |
||||
|
jest.mock('../../../src/utils/host-name'); |
||||
|
|
||||
|
describe('Editor telemetry', () => { |
||||
|
const version = '1.0.0'; |
||||
|
let fixture: HTMLElement; |
||||
|
let editorName = ''; |
||||
|
let htmlString = ''; |
||||
|
let config: Partial<EditorConfig>; |
||||
|
let cssString = ''; |
||||
|
let documentEl = ''; |
||||
|
|
||||
|
let originalFetch: typeof fetch; |
||||
|
let fetchMock: jest.Mock; |
||||
|
|
||||
|
const initTestEditor = (config: Partial<EditorConfig>) => { |
||||
|
grapesjs.version = version; |
||||
|
const editor = grapesjs.init({ |
||||
|
...config, |
||||
|
plugins: [fixJsDom, ...(config.plugins || [])], |
||||
|
}); |
||||
|
fixJsDomIframe(editor.getModel().shallow); |
||||
|
|
||||
|
return editor; |
||||
|
}; |
||||
|
|
||||
|
beforeAll(() => { |
||||
|
jest.spyOn(hostUtil, 'getHostName').mockReturnValue('example.com'); |
||||
|
editorName = 'editor-fixture'; |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
const initHtml = '<div class="test1"></div><div class="test2"></div>'; |
||||
|
htmlString = `<body>${initHtml}</body>`; |
||||
|
cssString = '.test2{color:red}.test3{color:blue}'; |
||||
|
documentEl = '<style>' + cssString + '</style>' + initHtml; |
||||
|
config = { |
||||
|
container: '#' + editorName, |
||||
|
storageManager: { |
||||
|
autoload: false, |
||||
|
autosave: false, |
||||
|
type: '', |
||||
|
}, |
||||
|
}; |
||||
|
document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`; |
||||
|
fixture = document.body.querySelector(`#${editorName}`)!; |
||||
|
|
||||
|
originalFetch = global.fetch; |
||||
|
fetchMock = jest.fn(() => Promise.resolve({ ok: true })); |
||||
|
global.fetch = fetchMock; |
||||
|
|
||||
|
const sessionStorageMock = { |
||||
|
getItem: jest.fn(), |
||||
|
setItem: jest.fn(), |
||||
|
removeItem: jest.fn(), |
||||
|
}; |
||||
|
|
||||
|
Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock }); |
||||
|
|
||||
|
Object.defineProperty(window, 'location', { |
||||
|
value: { |
||||
|
hostname: 'example.com', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
console.log = jest.fn(); |
||||
|
console.error = jest.fn(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
global.fetch = originalFetch; |
||||
|
jest.resetAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
test('Telemetry is sent when enabled', async () => { |
||||
|
const editor = initTestEditor({ |
||||
|
...config, |
||||
|
telemetry: true, |
||||
|
}); |
||||
|
|
||||
|
await waitEditorEvent(editor, 'load'); |
||||
|
|
||||
|
expect(fetchMock).toHaveBeenCalledTimes(1); |
||||
|
expect(fetchMock.mock.calls[0][0]).toContain('/api/gjs/telemetry/collect'); |
||||
|
expect(fetchMock.mock.calls[0][1].method).toBe('POST'); |
||||
|
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({ |
||||
|
domain: expect.any(String), |
||||
|
version: expect.any(String), |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
test('Telemetry is not sent when disabled', async () => { |
||||
|
const editor = initTestEditor({ |
||||
|
...config, |
||||
|
telemetry: false, |
||||
|
}); |
||||
|
await waitEditorEvent(editor, 'load'); |
||||
|
|
||||
|
expect(fetchMock).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
test('Telemetry is not sent twice in the same session', async () => { |
||||
|
window.sessionStorage.getItem = jest.fn(() => 'true'); |
||||
|
|
||||
|
const editor = initTestEditor({ |
||||
|
...config, |
||||
|
telemetry: true, |
||||
|
}); |
||||
|
await waitEditorEvent(editor, 'load'); |
||||
|
|
||||
|
expect(fetchMock).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
test('Telemetry handles fetch errors gracefully', async () => { |
||||
|
fetchMock.mockRejectedValueOnce(new Error('Network error')); |
||||
|
|
||||
|
const editor = initTestEditor({ |
||||
|
...config, |
||||
|
telemetry: true, |
||||
|
}); |
||||
|
await waitEditorEvent(editor, 'load'); |
||||
|
|
||||
|
expect(fetchMock).toHaveBeenCalledTimes(1); |
||||
|
expect(console.log).not.toHaveBeenCalled(); |
||||
|
expect(console.error).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
test('Telemetry cleans up old version keys', async () => { |
||||
|
const sessionStorageMock = { |
||||
|
getItem: jest.fn(() => null), |
||||
|
setItem: jest.fn(), |
||||
|
removeItem: jest.fn(), |
||||
|
'gjs_telemetry_sent_0.9.0': 'true', |
||||
|
'gjs_telemetry_sent_0.9.1': 'true', |
||||
|
other_key: 'true', |
||||
|
}; |
||||
|
Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock }); |
||||
|
Object.defineProperty(sessionStorageMock, 'length', { value: 3 }); |
||||
|
|
||||
|
fetchMock.mockResolvedValueOnce({ ok: true }); |
||||
|
|
||||
|
const editor = initTestEditor({ |
||||
|
...config, |
||||
|
telemetry: true, |
||||
|
}); |
||||
|
await waitEditorEvent(editor, 'load'); |
||||
|
|
||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
||||
|
expect(sessionStorageMock.setItem).toHaveBeenCalledWith(`gjs_telemetry_sent_${version}`, 'true'); |
||||
|
expect(sessionStorageMock.removeItem).toHaveBeenCalledWith('gjs_telemetry_sent_0.9.0'); |
||||
|
expect(sessionStorageMock.removeItem).toHaveBeenCalledWith('gjs_telemetry_sent_0.9.1'); |
||||
|
expect(sessionStorageMock.removeItem).not.toHaveBeenCalledWith('other_key'); |
||||
|
}, 10000); |
||||
|
}); |
||||
Loading…
Reference in new issue