Merge commit 'af0cd00b2862212486d3e6c6fd3ab16e1e6f63ea' of https://github.com/GitStartHQ/strapi into fix/add-button-repeatable-components

This commit is contained in:
gitstart 2022-12-14 13:48:13 +00:00
commit f3664d53b4
37 changed files with 469 additions and 246 deletions

View File

@ -20,7 +20,7 @@ import {
fetchUserRoles, fetchUserRoles,
} from './utils/api'; } from './utils/api';
import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion'; import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion';
import { getFullName } from '../../utils'; import { getFullName, hashAdminUserEmail } from '../../utils';
const strapiVersion = packageJSON.version; const strapiVersion = packageJSON.version;
@ -31,6 +31,7 @@ const AuthenticatedApp = () => {
const userInfo = auth.getUserInfo(); const userInfo = auth.getUserInfo();
const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname); const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname);
const [userDisplayName, setUserDisplayName] = useState(userName); const [userDisplayName, setUserDisplayName] = useState(userName);
const [userId, setUserId] = useState(null);
const { showReleaseNotification } = useConfigurations(); const { showReleaseNotification } = useConfigurations();
const [ const [
{ data: appInfos, status }, { data: appInfos, status },
@ -71,6 +72,15 @@ const AuthenticatedApp = () => {
} }
}, [userRoles, appInfos]); }, [userRoles, appInfos]);
useEffect(() => {
const getUserId = async () => {
const userId = await hashAdminUserEmail(userInfo);
setUserId(userId);
};
getUserId();
}, [userInfo]);
// We don't need to wait for the release query to be fetched before rendering the plugins // We don't need to wait for the release query to be fetched before rendering the plugins
// however, we need the appInfos and the permissions // however, we need the appInfos and the permissions
const shouldShowNotDependentQueriesLoader = const shouldShowNotDependentQueriesLoader =
@ -81,12 +91,13 @@ const AuthenticatedApp = () => {
const appInfosValue = useMemo(() => { const appInfosValue = useMemo(() => {
return { return {
...appInfos, ...appInfos,
userId,
latestStrapiReleaseTag: tag_name, latestStrapiReleaseTag: tag_name,
setUserDisplayName, setUserDisplayName,
shouldUpdateStrapi, shouldUpdateStrapi,
userDisplayName, userDisplayName,
}; };
}, [appInfos, tag_name, shouldUpdateStrapi, userDisplayName]); }, [appInfos, tag_name, shouldUpdateStrapi, userDisplayName, userId]);
if (shouldShowLoader) { if (shouldShowLoader) {
return <LoadingIndicatorPage />; return <LoadingIndicatorPage />;

View File

@ -20,7 +20,9 @@ const strapiVersion = packageJSON.version;
jest.mock('@strapi/helper-plugin', () => ({ jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'), ...jest.requireActual('@strapi/helper-plugin'),
auth: { getUserInfo: () => ({ firstname: 'kai', lastname: 'doe' }) }, auth: {
getUserInfo: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
},
useGuidedTour: jest.fn(() => ({ useGuidedTour: jest.fn(() => ({
setGuidedTourVisibility: jest.fn(), setGuidedTourVisibility: jest.fn(),
})), })),

View File

@ -24,18 +24,13 @@ import { RELATION_ITEM_HEIGHT } from './constants';
import { usePrev } from '../../hooks'; import { usePrev } from '../../hooks';
const LinkEllipsis = styled(Link)` const LinkEllipsis = styled(Link)`
white-space: nowrap; display: block;
overflow: hidden;
text-overflow: ellipsis;
display: inherit;
`;
const BoxEllipsis = styled(Box)`
> span { > span {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: inherit; display: block;
} }
`; `;
@ -322,7 +317,7 @@ const RelationInput = ({
} }
style={style} style={style}
> >
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}> <Box minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
<Tooltip description={mainField ?? `${id}`}> <Tooltip description={mainField ?? `${id}`}>
{href ? ( {href ? (
<LinkEllipsis to={href} disabled={disabled}> <LinkEllipsis to={href} disabled={disabled}>
@ -334,7 +329,7 @@ const RelationInput = ({
</Typography> </Typography>
)} )}
</Tooltip> </Tooltip>
</BoxEllipsis> </Box>
{publicationState && ( {publicationState && (
<Status variant={statusColor} showBullet={false} size="S"> <Status variant={statusColor} showBullet={false} size="S">

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Content-Manager || RelationInput should render and match snapshot 1`] = ` exports[`Content-Manager || RelationInput should render and match snapshot 1`] = `
.c36 { .c35 {
border: 0; border: 0;
-webkit-clip: rect(0 0 0 0); -webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
@ -49,7 +49,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
min-width: 0px; min-width: 0px;
} }
.c23 { .c22 {
background: #eaf5ff; background: #eaf5ff;
padding-top: 4px; padding-top: 4px;
padding-right: 8px; padding-right: 8px;
@ -60,16 +60,16 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
border: 1px solid #b8e1ff; border: 1px solid #b8e1ff;
} }
.c26 { .c25 {
padding-left: 16px; padding-left: 16px;
} }
.c28 { .c27 {
color: #666687; color: #666687;
width: 12px; width: 12px;
} }
.c31 { .c30 {
background: #eafbe7; background: #eafbe7;
padding-top: 4px; padding-top: 4px;
padding-right: 8px; padding-right: 8px;
@ -80,7 +80,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
border: 1px solid #c6f0c2; border: 1px solid #c6f0c2;
} }
.c34 { .c33 {
padding-top: 8px; padding-top: 8px;
} }
@ -165,20 +165,20 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
color: #4945ff; color: #4945ff;
} }
.c22 { .c21 {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.43; line-height: 1.43;
color: #32324d; color: #32324d;
} }
.c25 { .c24 {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.43; line-height: 1.43;
font-weight: 600; font-weight: 600;
color: #006096; color: #006096;
} }
.c30 { .c29 {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.43; line-height: 1.43;
display: block; display: block;
@ -188,14 +188,14 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
color: #4945ff; color: #4945ff;
} }
.c33 { .c32 {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.43; line-height: 1.43;
font-weight: 600; font-weight: 600;
color: #2f6846; color: #2f6846;
} }
.c35 { .c34 {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.33; line-height: 1.33;
color: #666687; color: #666687;
@ -210,7 +210,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
margin-top: 4px; margin-top: 4px;
} }
.c29 path { .c28 path {
fill: #666687; fill: #666687;
} }
@ -300,15 +300,15 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
width: 0.5625rem; width: 0.5625rem;
} }
.c24 .c5 { .c23 .c5 {
color: #0c75af; color: #0c75af;
} }
.c32 .c5 { .c31 .c5 {
color: #328048; color: #328048;
} }
.c20 { .c19 {
display: -webkit-inline-box; display: -webkit-inline-box;
display: -webkit-inline-flex; display: -webkit-inline-flex;
display: -ms-inline-flexbox; display: -ms-inline-flexbox;
@ -324,31 +324,31 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
outline: none; outline: none;
} }
.c20 svg path { .c19 svg path {
-webkit-transition: fill 150ms ease-out; -webkit-transition: fill 150ms ease-out;
transition: fill 150ms ease-out; transition: fill 150ms ease-out;
fill: currentColor; fill: currentColor;
} }
.c20 svg { .c19 svg {
font-size: 0.625rem; font-size: 0.625rem;
} }
.c20 .c5 { .c19 .c5 {
-webkit-transition: color 150ms ease-out; -webkit-transition: color 150ms ease-out;
transition: color 150ms ease-out; transition: color 150ms ease-out;
color: currentColor; color: currentColor;
} }
.c20:hover { .c19:hover {
color: #7b79ff; color: #7b79ff;
} }
.c20:active { .c19:active {
color: #271fe0; color: #271fe0;
} }
.c20:after { .c19:after {
-webkit-transition-property: all; -webkit-transition-property: all;
transition-property: all; transition-property: all;
-webkit-transition-duration: 0.2s; -webkit-transition-duration: 0.2s;
@ -363,11 +363,11 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
border: 2px solid transparent; border: 2px solid transparent;
} }
.c20:focus-visible { .c19:focus-visible {
outline: none; outline: none;
} }
.c20:focus-visible:after { .c19:focus-visible:after {
border-radius: 8px; border-radius: 8px;
content: ''; content: '';
position: absolute; position: absolute;
@ -407,26 +407,23 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
bottom: 0; bottom: 0;
} }
.c21 { .c20 {
display: block;
}
.c20 > span {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: inherit; display: block;
} }
.c19 > span { .c26 svg path {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inherit;
}
.c27 svg path {
fill: #8e8ea9; fill: #8e8ea9;
} }
.c27:hover svg path, .c26:hover svg path,
.c27:focus svg path { .c26:focus svg path {
fill: #666687; fill: #666687;
} }
@ -585,18 +582,18 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
class="c16 c17" class="c16 c17"
> >
<div <div
class="c18 c19" class="c18"
> >
<span> <span>
<a <a
aria-current="page" aria-current="page"
aria-describedby="tooltip-1" aria-describedby="tooltip-1"
class="c20 c21 active" class="c19 c20 active"
href="/" href="/"
tabindex="0" tabindex="0"
> >
<span <span
class="c5 c22" class="c5 c21"
> >
Relation 1 Relation 1
</span> </span>
@ -604,26 +601,26 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
</span> </span>
</div> </div>
<div <div
class="c23 c24" class="c22 c23"
> >
<span <span
class="c5 c25" class="c5 c24"
> >
Draft Draft
</span> </span>
</div> </div>
</div> </div>
<div <div
class="c26" class="c25"
> >
<button <button
aria-label="Remove" aria-label="Remove"
class="c27" class="c26"
data-testid="remove-relation-1" data-testid="remove-relation-1"
type="button" type="button"
> >
<svg <svg
class="c28 c29" class="c27 c28"
fill="none" fill="none"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -650,12 +647,12 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
class="c16 c17" class="c16 c17"
> >
<div <div
class="c18 c19" class="c18"
> >
<span> <span>
<span <span
aria-describedby="tooltip-3" aria-describedby="tooltip-3"
class="c5 c30" class="c5 c29"
tabindex="0" tabindex="0"
> >
Relation 2 Relation 2
@ -663,26 +660,26 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
</span> </span>
</div> </div>
<div <div
class="c31 c32" class="c30 c31"
> >
<span <span
class="c5 c33" class="c5 c32"
> >
Published Published
</span> </span>
</div> </div>
</div> </div>
<div <div
class="c26" class="c25"
> >
<button <button
aria-label="Remove" aria-label="Remove"
class="c27" class="c26"
data-testid="remove-relation-2" data-testid="remove-relation-2"
type="button" type="button"
> >
<svg <svg
class="c28 c29" class="c27 c28"
fill="none" fill="none"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -709,12 +706,12 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
class="c16 c17" class="c16 c17"
> >
<div <div
class="c18 c19" class="c18"
> >
<span> <span>
<span <span
aria-describedby="tooltip-5" aria-describedby="tooltip-5"
class="c5 c30" class="c5 c29"
tabindex="0" tabindex="0"
> >
Relation 3 Relation 3
@ -723,16 +720,16 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
</div> </div>
</div> </div>
<div <div
class="c26" class="c25"
> >
<button <button
aria-label="Remove" aria-label="Remove"
class="c27" class="c26"
data-testid="remove-relation-3" data-testid="remove-relation-3"
type="button" type="button"
> >
<svg <svg
class="c28 c29" class="c27 c28"
fill="none" fill="none"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -753,10 +750,10 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
</div> </div>
</div> </div>
<div <div
class="c34" class="c33"
> >
<p <p
class="c5 c35" class="c5 c34"
id="1-hint" id="1-hint"
> >
this is a description this is a description
@ -764,7 +761,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
</div> </div>
</div> </div>
<div <div
class="c36" class="c35"
> >
<p <p
aria-live="polite" aria-live="polite"

View File

@ -35,7 +35,10 @@ function App() {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { updateProjectSettings } = useConfigurations(); const { updateProjectSettings } = useConfigurations();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [{ isLoading, hasAdmin, uuid }, setState] = useState({ isLoading: true, hasAdmin: false }); const [{ isLoading, hasAdmin, uuid, deviceId }, setState] = useState({
isLoading: true,
hasAdmin: false,
});
const appInfo = useAppInfos(); const appInfo = useAppInfos();
const { get } = useFetchClient(); const { get } = useFetchClient();
@ -81,6 +84,7 @@ function App() {
} = await axios.get(`${strapi.backendURL}/admin/init`); } = await axios.get(`${strapi.backendURL}/admin/init`);
updateProjectSettings({ menuLogo: prefixFileUrlWithBackendUrl(menuLogo) }); updateProjectSettings({ menuLogo: prefixFileUrlWithBackendUrl(menuLogo) });
const deviceId = await getUID();
if (uuid) { if (uuid) {
const { const {
@ -93,18 +97,16 @@ function App() {
setTelemetryProperties(properties); setTelemetryProperties(properties);
try { try {
const deviceId = await getUID(); await fetch('https://analytics.strapi.io/api/v2/track', {
await fetch('https://analytics.strapi.io/track', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
// This event is anonymous
event: 'didInitializeAdministration', event: 'didInitializeAdministration',
uuid, userId: '',
deviceId, deviceId,
properties: { eventPropeties: {},
...properties, userProperties: { environment: appInfo.currentEnvironment },
environment: appInfo.currentEnvironment, groupProperties: { ...properties, projectId: uuid },
},
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -115,7 +117,7 @@ function App() {
} }
} }
setState({ isLoading: false, hasAdmin, uuid }); setState({ isLoading: false, hasAdmin, uuid, deviceId });
} catch (err) { } catch (err) {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
@ -134,8 +136,9 @@ function App() {
() => ({ () => ({
uuid, uuid,
telemetryProperties, telemetryProperties,
deviceId,
}), }),
[uuid, telemetryProperties] [uuid, telemetryProperties, deviceId]
); );
if (isLoading) { if (isLoading) {

View File

@ -7,3 +7,4 @@ export { default as sortLinks } from './sortLinks';
export { default as getExistingActions } from './getExistingActions'; export { default as getExistingActions } from './getExistingActions';
export { default as getRequestUrl } from './getRequestUrl'; export { default as getRequestUrl } from './getRequestUrl';
export { default as getFullName } from './getFullName'; export { default as getFullName } from './getFullName';
export { default as hashAdminUserEmail } from './uniqueAdminHash';

View File

@ -0,0 +1,49 @@
import crypto from 'crypto';
import { TextEncoder } from 'util';
import hashAdminUserEmail, { utils } from '../uniqueAdminHash';
const testHashValue = '8544bf5b5389959462912699664f03ed664a4b6d24f03b13bdbc362efc147873';
describe('Creating admin user email hash in admin', () => {
afterAll(() => {
Object.defineProperty(global.self, 'crypto', {
value: undefined,
});
Object.defineProperty(global.self, 'TextEncoder', {
value: undefined,
});
});
it('should return empty string if no payload provided', async () => {
const testHash = await hashAdminUserEmail();
expect(testHash).toBe(null);
});
it('should return hash using crypto subtle', async () => {
Object.defineProperty(global.self, 'crypto', {
value: {
subtle: {
digest: jest.fn((type, message) => crypto.createHash('sha256').update(message).digest()),
},
},
configurable: true,
});
Object.defineProperty(global.self, 'TextEncoder', {
value: TextEncoder,
configurable: true,
});
const payload = {
email: 'testemail@strapi.io',
};
const spy = jest.spyOn(utils, 'digestMessage');
const testHash = await hashAdminUserEmail(payload);
expect(spy).toHaveBeenCalled();
expect(testHash).toBe(testHashValue);
});
});

View File

@ -0,0 +1,22 @@
export const utils = {
bufferToHex(buffer) {
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
},
async digestMessage(message) {
const msgUint8 = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
return this.bufferToHex(hashBuffer);
},
};
export default async function hashAdminUserEmail(payload) {
if (!payload) {
return null;
}
try {
return await utils.digestMessage(payload.email);
} catch (error) {
return null;
}
}

View File

@ -69,6 +69,7 @@ describe('Admin Controller', () => {
'info.dependencies': { 'info.dependencies': {
dependency: '1.0.0', dependency: '1.0.0',
}, },
uuid: 'testuuid',
environment: 'development', environment: 'development',
}[key] || value) }[key] || value)
), ),
@ -85,12 +86,14 @@ describe('Admin Controller', () => {
['autoReload', false], ['autoReload', false],
['info.strapi', null], ['info.strapi', null],
['info.dependencies', {}], ['info.dependencies', {}],
['uuid', null],
]); ]);
expect(result.data).toBeDefined(); expect(result.data).toBeDefined();
expect(result.data).toStrictEqual({ expect(result.data).toStrictEqual({
currentEnvironment: 'development', currentEnvironment: 'development',
autoReload: false, autoReload: false,
strapiVersion: '1.0.0', strapiVersion: '1.0.0',
projectId: 'testuuid',
dependencies: { dependencies: {
dependency: '1.0.0', dependency: '1.0.0',
}, },

View File

@ -110,6 +110,7 @@ module.exports = {
const autoReload = strapi.config.get('autoReload', false); const autoReload = strapi.config.get('autoReload', false);
const strapiVersion = strapi.config.get('info.strapi', null); const strapiVersion = strapi.config.get('info.strapi', null);
const dependencies = strapi.config.get('info.dependencies', {}); const dependencies = strapi.config.get('info.dependencies', {});
const projectId = strapi.config.get('uuid', null);
const nodeVersion = process.version; const nodeVersion = process.version;
const communityEdition = !strapi.EE; const communityEdition = !strapi.EE;
const useYarn = await exists(path.join(process.cwd(), 'yarn.lock')); const useYarn = await exists(path.join(process.cwd(), 'yarn.lock'));
@ -120,6 +121,7 @@ module.exports = {
autoReload, autoReload,
strapiVersion, strapiVersion,
dependencies, dependencies,
projectId,
nodeVersion, nodeVersion,
communityEdition, communityEdition,
useYarn, useYarn,

View File

@ -19,7 +19,12 @@ describe('Metrics', () => {
await metricsService.sendDidInviteUser(); await metricsService.sendDidInviteUser();
expect(send).toHaveBeenCalledWith('didInviteUser', { numberOfRoles: 3, numberOfUsers: 2 }); expect(send).toHaveBeenCalledWith('didInviteUser', {
groupProperties: {
numberOfRoles: 3,
numberOfUsers: 2,
},
});
expect(countUsers).toHaveBeenCalledWith(); expect(countUsers).toHaveBeenCalledWith();
expect(countRoles).toHaveBeenCalledWith(); expect(countRoles).toHaveBeenCalledWith();
}); });
@ -52,7 +57,9 @@ describe('Metrics', () => {
expect(getLanguagesInUse).toHaveBeenCalledWith(); expect(getLanguagesInUse).toHaveBeenCalledWith();
expect(send).toHaveBeenCalledWith('didChangeInterfaceLanguage', { expect(send).toHaveBeenCalledWith('didChangeInterfaceLanguage', {
languagesInUse: ['en', 'fr', 'en'], userProperties: {
languagesInUse: ['en', 'fr', 'en'],
},
}); });
}); });
}); });

View File

@ -5,7 +5,9 @@ const { getService } = require('../utils');
const sendDidInviteUser = async () => { const sendDidInviteUser = async () => {
const numberOfUsers = await getService('user').count(); const numberOfUsers = await getService('user').count();
const numberOfRoles = await getService('role').count(); const numberOfRoles = await getService('role').count();
strapi.telemetry.send('didInviteUser', { numberOfRoles, numberOfUsers }); strapi.telemetry.send('didInviteUser', {
groupProperties: { numberOfRoles, numberOfUsers },
});
}; };
const sendDidUpdateRolePermissions = async () => { const sendDidUpdateRolePermissions = async () => {
@ -14,7 +16,8 @@ const sendDidUpdateRolePermissions = async () => {
const sendDidChangeInterfaceLanguage = async () => { const sendDidChangeInterfaceLanguage = async () => {
const languagesInUse = await getService('user').getLanguagesInUse(); const languagesInUse = await getService('user').getLanguagesInUse();
strapi.telemetry.send('didChangeInterfaceLanguage', { languagesInUse }); // This event is anonymous
strapi.telemetry.send('didChangeInterfaceLanguage', { userProperties: { languagesInUse } });
}; };
module.exports = { module.exports = {

View File

@ -85,6 +85,7 @@ describe('Single Types', () => {
}, },
user: { user: {
id: 1, id: 1,
email: 'someTestEmailString',
}, },
}; };
@ -180,7 +181,9 @@ describe('Single Types', () => {
); );
expect(sendTelemetry).toHaveBeenCalledWith('didCreateFirstContentTypeEntry', { expect(sendTelemetry).toHaveBeenCalledWith('didCreateFirstContentTypeEntry', {
model: modelUid, eventProperties: {
model: modelUid,
},
}); });
}); });

View File

@ -85,7 +85,9 @@ module.exports = {
ctx.body = await permissionChecker.sanitizeOutput(entity); ctx.body = await permissionChecker.sanitizeOutput(entity);
if (totalEntries === 0) { if (totalEntries === 0) {
strapi.telemetry.send('didCreateFirstContentTypeEntry', { model }); strapi.telemetry.send('didCreateFirstContentTypeEntry', {
eventProperties: { model },
});
} }
}, },

View File

@ -73,7 +73,9 @@ module.exports = {
const newEntity = await entityManager.create(sanitizedBody, model, { params: query }); const newEntity = await entityManager.create(sanitizedBody, model, { params: query });
ctx.body = await permissionChecker.sanitizeOutput(newEntity); ctx.body = await permissionChecker.sanitizeOutput(newEntity);
await strapi.telemetry.send('didCreateFirstContentTypeEntry', { model }); await strapi.telemetry.send('didCreateFirstContentTypeEntry', {
eventProperties: { model },
});
return; return;
} }

View File

@ -66,14 +66,15 @@ describe('metrics', () => {
global.strapi = { telemetry: { send } }; global.strapi = { telemetry: { send } };
metricsService = metricsServiceLoader({ strapi }); metricsService = metricsServiceLoader({ strapi });
const [containsRelationalFields, displayedFields, displayedRelationalFields] = expectedResult; const [containsRelationalFields, displayedFields, displayedRelationalFields] = expectedResult;
await metricsService.sendDidConfigureListView(contentType, { layouts: { list } }); await metricsService.sendDidConfigureListView(contentType, { layouts: { list } });
expect(send).toHaveBeenCalledTimes(1); expect(send).toHaveBeenCalledTimes(1);
expect(send).toHaveBeenCalledWith('didConfigureListView', { expect(send).toHaveBeenCalledWith('didConfigureListView', {
displayedFields, eventProperties: {
containsRelationalFields, containsRelationalFields,
displayedRelationalFields, displayedFields,
displayedRelationalFields,
},
}); });
}); });
}); });

View File

@ -13,11 +13,11 @@ module.exports = ({ strapi }) => {
).length; ).length;
const data = { const data = {
containsRelationalFields: !!displayedRelationalFields, eventProperties: { containsRelationalFields: !!displayedRelationalFields },
}; };
if (data.containsRelationalFields) { if (data.eventProperties.containsRelationalFields) {
Object.assign(data, { Object.assign(data.eventProperties, {
displayedFields, displayedFields,
displayedRelationalFields, displayedRelationalFields,
}); });

View File

@ -64,15 +64,17 @@ module.exports = {
components: body.components, components: body.components,
}); });
const metricsProperties = { const metricsPayload = {
kind: contentType.kind, eventProperties: {
hasDraftAndPublish: hasDraftAndPublish(contentType.schema), kind: contentType.kind,
hasDraftAndPublish: hasDraftAndPublish(contentType.schema),
},
}; };
if (_.isEmpty(strapi.api)) { if (_.isEmpty(strapi.api)) {
await strapi.telemetry.send('didCreateFirstContentType', metricsProperties); await strapi.telemetry.send('didCreateFirstContentType', metricsPayload);
} else { } else {
await strapi.telemetry.send('didCreateContentType', metricsProperties); await strapi.telemetry.send('didCreateContentType', metricsPayload);
} }
setImmediate(() => strapi.reload()); setImmediate(() => strapi.reload());
@ -80,7 +82,9 @@ module.exports = {
ctx.send({ data: { uid: contentType.uid } }, 201); ctx.send({ data: { uid: contentType.uid } }, 201);
} catch (error) { } catch (error) {
strapi.log.error(error); strapi.log.error(error);
await strapi.telemetry.send('didNotCreateContentType', { error: error.message }); await strapi.telemetry.send('didNotCreateContentType', {
eventProperties: { error: error.message },
});
ctx.send({ error: error.message }, 400); ctx.send({ error: error.message }, 400);
} }
}, },

View File

@ -198,60 +198,42 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
return; return;
} }
// Handle databases that don't support window function ROW_NUMBER
if (!strapi.db.dialect.supportsWindowFunctions()) {
await cleanOrderColumnsForOldDatabases({ id, attribute, db, inverseRelIds, transaction: trx });
return;
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const update = [];
const updateBinding = [];
const select = ['??'];
const selectBinding = ['id'];
const where = [];
const whereBinding = [];
if (hasOrderColumn(attribute) && id) {
update.push('?? = b.src_order');
updateBinding.push(orderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
selectBinding.push(joinColumn.name, orderColumnName);
where.push('?? = ?');
whereBinding.push(joinColumn.name, id);
}
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
update.push('?? = b.inv_order');
updateBinding.push(inverseOrderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
}
// raw query as knex doesn't allow updating from a subquery
// https://github.com/knex/knex/issues/2504
switch (strapi.db.dialect.client) { switch (strapi.db.dialect.client) {
case 'mysql': case 'mysql':
await db.connection await cleanOrderColumnsForInnoDB({ id, attribute, db, inverseRelIds, transaction: trx });
.raw(
`UPDATE
?? as a,
(
SELECT ${select.join(', ')}
FROM ??
WHERE ${where.join(' OR ')}
) AS b
SET ${update.join(', ')}
WHERE b.id = a.id`,
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
)
.transacting(trx);
break; break;
default: { default: {
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const update = [];
const updateBinding = [];
const select = ['??'];
const selectBinding = ['id'];
const where = [];
const whereBinding = [];
if (hasOrderColumn(attribute) && id) {
update.push('?? = b.src_order');
updateBinding.push(orderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
selectBinding.push(joinColumn.name, orderColumnName);
where.push('?? = ?');
whereBinding.push(joinColumn.name, id);
}
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
update.push('?? = b.inv_order');
updateBinding.push(inverseOrderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
}
const joinTableName = addSchema(joinTable.name); const joinTableName = addSchema(joinTable.name);
// raw query as knex doesn't allow updating from a subquery
// https://github.com/knex/knex/issues/2504
await db.connection await db.connection
.raw( .raw(
`UPDATE ?? as a `UPDATE ?? as a
@ -265,24 +247,29 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
[joinTableName, ...updateBinding, ...selectBinding, joinTableName, ...whereBinding] [joinTableName, ...updateBinding, ...selectBinding, joinTableName, ...whereBinding]
) )
.transacting(trx); .transacting(trx);
/*
`UPDATE :joinTable: as a
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
FROM (
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable:
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
) AS b
WHERE b.id = a.id`,
*/
} }
/*
`UPDATE :joinTable: as a
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
FROM (
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable:
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
) AS b
WHERE b.id = a.id`,
*/
} }
}; };
const cleanOrderColumnsForOldDatabases = async ({ /*
* Ensure that orders are following a 1, 2, 3 sequence, without gap.
* The use of a temporary table instead of a window function makes the query compatible with MySQL 5 and prevents some deadlocks to happen in innoDB databases
*/
const cleanOrderColumnsForInnoDB = async ({
id, id,
attribute, attribute,
db, db,
@ -319,6 +306,9 @@ const cleanOrderColumnsForOldDatabases = async ({
} }
) )
.transacting(trx); .transacting(trx);
// raw query as knex doesn't allow updating from a subquery
// https://github.com/knex/knex/issues/2504
await db.connection await db.connection
.raw( .raw(
`UPDATE ?? as a, (SELECT * FROM ??) AS b `UPDATE ?? as a, (SELECT * FROM ??) AS b

View File

@ -5,22 +5,31 @@ import useAppInfos from '../useAppInfos';
const useTracking = () => { const useTracking = () => {
const trackRef = useRef(); const trackRef = useRef();
const { uuid, telemetryProperties } = useContext(TrackingContext); const { uuid, telemetryProperties, deviceId } = useContext(TrackingContext);
const appInfo = useAppInfos(); const appInfo = useAppInfos();
const userId = appInfo?.userId;
trackRef.current = async (event, properties) => { trackRef.current = async (event, properties) => {
if (uuid && !window.strapi.telemetryDisabled) { if (uuid && !window.strapi.telemetryDisabled) {
try { try {
await axios.post('https://analytics.strapi.io/track', { await axios.post(
event, 'https://analytics.strapi.io/api/v2/track',
properties: { {
...telemetryProperties, event,
...properties, userId,
projectType: window.strapi.projectType, deviceId,
environment: appInfo.currentEnvironment, eventProperties: { ...properties },
userProperties: {},
groupProperties: {
...telemetryProperties,
projectId: uuid,
projectType: window.strapi.projectType,
},
}, },
uuid, {
}); headers: { 'Content-Type': 'application/json' },
}
);
} catch (err) { } catch (err) {
// Silent // Silent
} }

View File

@ -25,6 +25,7 @@ function setup(props) {
telemetryProperties: { telemetryProperties: {
nestedProperty: true, nestedProperty: true,
}, },
deviceId: 'someTestDeviceId',
...props, ...props,
}} }}
> >
@ -45,21 +46,33 @@ describe('useTracking', () => {
test('Call trackUsage() with all attributes', async () => { test('Call trackUsage() with all attributes', async () => {
useAppInfos.mockReturnValue({ useAppInfos.mockReturnValue({
currentEnvironment: 'testing', currentEnvironment: 'testing',
userId: 'someTestUserId',
}); });
const { result } = await setup(); const { result } = await setup();
result.current.trackUsage('event', { trackingProperty: true }); result.current.trackUsage('event', { trackingProperty: true });
expect(axios.post).toBeCalledWith(expect.any(String), { expect(axios.post).toBeCalledWith(
event: 'event', expect.any(String),
uuid: 1, {
properties: expect.objectContaining({ userId: 'someTestUserId',
environment: 'testing', deviceId: 'someTestDeviceId',
nestedProperty: true, event: 'event',
trackingProperty: true, eventProperties: {
}), trackingProperty: true,
}); },
groupProperties: {
nestedProperty: true,
projectId: 1,
projectType: 'Community',
},
userProperties: {},
},
{
headers: { 'Content-Type': 'application/json' },
}
);
}); });
test('Do not track if it has been disabled', async () => { test('Do not track if it has been disabled', async () => {

View File

@ -242,11 +242,14 @@ class Strapi {
sendStartupTelemetry() { sendStartupTelemetry() {
// Emit started event. // Emit started event.
// do not await to avoid slower startup // do not await to avoid slower startup
// This event is anonymous
this.telemetry.send('didStartServer', { this.telemetry.send('didStartServer', {
database: strapi.config.get('database.connection.client'), groupProperties: {
plugins: Object.keys(strapi.plugins), database: strapi.config.get('database.connection.client'),
// TODO: to add back plugins: Object.keys(strapi.plugins),
// providers: this.config.installedProviders, // TODO: to add back
// providers: this.config.installedProviders,
},
}); });
} }

View File

@ -53,12 +53,12 @@ const generateNewPackageJSON = (packageObj) => {
const sendEvent = async (uuid) => { const sendEvent = async (uuid) => {
try { try {
await fetch('https://analytics.strapi.io/track', { await fetch('https://analytics.strapi.io/api/v2/track', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
event: 'didOptInTelemetry', event: 'didOptInTelemetry',
uuid,
deviceId: machineID(), deviceId: machineID(),
groupProperties: { projectId: uuid },
}), }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });

View File

@ -28,12 +28,12 @@ const writePackageJSON = async (path, file, spacing) => {
const sendEvent = async (uuid) => { const sendEvent = async (uuid) => {
try { try {
await fetch('https://analytics.strapi.io/track', { await fetch('https://analytics.strapi.io/api/v2/track', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
event: 'didOptOutTelemetry', event: 'didOptOutTelemetry',
uuid,
deviceId: machineID(), deviceId: machineID(),
groupProperties: { projectId: uuid },
}), }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });

View File

@ -0,0 +1,41 @@
'use strict';
const crypto = require('crypto');
const { generateAdminUserHash } = require('../admin-user-hash');
const createContext = require('../../../../../../../test/helpers/create-context');
describe('user email hash', () => {
test('should create a hash from admin user email', () => {
const state = {
user: {
email: 'testemail@strapi.io',
},
};
const ctx = createContext({}, { state });
global.strapi = {
requestContext: {
get: jest.fn(() => ctx),
},
};
const hash = crypto.createHash('sha256').update('testemail@strapi.io').digest('hex');
const userId = generateAdminUserHash();
expect(userId).toBe(hash);
});
test('should return empty string if user is not available on ctx', () => {
const ctx = createContext({}, {});
global.strapi = {
requestContext: {
get: jest.fn(() => ctx),
},
};
const userId = generateAdminUserHash();
expect(userId).toBe('');
});
});

View File

@ -29,6 +29,9 @@ describe('metrics', () => {
root: process.cwd(), root: process.cwd(),
}, },
}, },
requestContext: {
get: jest.fn(() => ({})),
},
}); });
metricsInstance.register(); metricsInstance.register();
@ -60,6 +63,9 @@ describe('metrics', () => {
root: process.cwd(), root: process.cwd(),
}, },
}, },
requestContext: {
get: jest.fn(() => ({})),
},
}); });
metricsInstance.register(); metricsInstance.register();
@ -89,18 +95,21 @@ describe('metrics', () => {
root: process.cwd(), root: process.cwd(),
}, },
}, },
requestContext: {
get: jest.fn(() => ({})),
},
}); });
send('someEvent'); send('someEvent');
expect(fetch).toHaveBeenCalled(); expect(fetch).toHaveBeenCalled();
expect(fetch.mock.calls[0][0]).toBe('https://analytics.strapi.io/track'); expect(fetch.mock.calls[0][0]).toBe('https://analytics.strapi.io/api/v2/track');
expect(fetch.mock.calls[0][1].method).toBe('POST'); expect(fetch.mock.calls[0][1].method).toBe('POST');
expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({ expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({
event: 'someEvent', event: 'someEvent',
uuid: 'test', groupProperties: {
properties: {
projectType: 'Community', projectType: 'Community',
projectId: 'test',
}, },
}); });
@ -128,6 +137,9 @@ describe('metrics', () => {
root: process.cwd(), root: process.cwd(),
}, },
}, },
requestContext: {
get: jest.fn(() => ({})),
},
}); });
send('someEvent'); send('someEvent');

View File

@ -0,0 +1,15 @@
'use strict';
const crypto = require('crypto');
const generateAdminUserHash = () => {
const ctx = strapi?.requestContext?.get();
if (!ctx?.state?.user) {
return '';
}
return crypto.createHash('sha256').update(ctx.state.user.email).digest('hex');
};
module.exports = {
generateAdminUserHash,
};

View File

@ -55,10 +55,12 @@ const createTelemetryInstance = (strapi) => {
return sendEvent( return sendEvent(
'didCheckLicense', 'didCheckLicense',
{ {
licenseInfo: { groupProperties: {
...ee.licenseInfo, licenseInfo: {
projectHash: hashProject(strapi), ...ee.licenseInfo,
dependencyHash: hashDep(strapi), projectHash: hashProject(strapi),
dependencyHash: hashDep(strapi),
},
}, },
}, },
{ {

View File

@ -19,7 +19,7 @@ const createMiddleware = ({ sendEvent }) => {
// Send max. 1000 events per day. // Send max. 1000 events per day.
if (_state.counter < 1000) { if (_state.counter < 1000) {
sendEvent('didReceiveRequest', { url: ctx.request.url }); sendEvent('didReceiveRequest', { eventProperties: { url: ctx.request.url } });
// Increase counter. // Increase counter.
_state.counter += 1; _state.counter += 1;

View File

@ -10,7 +10,7 @@ const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
const { env } = require('@strapi/utils'); const { env } = require('@strapi/utils');
const ee = require('../../utils/ee'); const ee = require('../../utils/ee');
const machineID = require('../../utils/machine-id'); const machineID = require('../../utils/machine-id');
const stringifyDeep = require('./stringify-deep'); const { generateAdminUserHash } = require('./admin-user-hash');
const defaultQueryOpts = { const defaultQueryOpts = {
timeout: 1000, timeout: 1000,
@ -42,41 +42,49 @@ module.exports = (strapi) => {
const serverRootPath = strapi.dirs.app.root; const serverRootPath = strapi.dirs.app.root;
const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin'); const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
const anonymousMetadata = { const anonymousUserProperties = {
environment: strapi.config.environment, environment: strapi.config.environment,
os: os.type(), os: os.type(),
osPlatform: os.platform(), osPlatform: os.platform(),
osArch: os.arch(), osArch: os.arch(),
osRelease: os.release(), osRelease: os.release(),
nodeVersion: process.versions.node, nodeVersion: process.versions.node,
};
const anonymousGroupProperties = {
docker: process.env.DOCKER || isDocker(), docker: process.env.DOCKER || isDocker(),
isCI: ciEnv.isCI, isCI: ciEnv.isCI,
version: strapi.config.get('info.strapi'), version: strapi.config.get('info.strapi'),
projectType: isEE ? 'Enterprise' : 'Community', projectType: isEE ? 'Enterprise' : 'Community',
useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath), useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath), useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
projectId: uuid,
isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud', isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud',
}; };
addPackageJsonStrapiMetadata(anonymousMetadata, strapi); addPackageJsonStrapiMetadata(anonymousGroupProperties, strapi);
return async (event, payload = {}, opts = {}) => { return async (event, payload = {}, opts = {}) => {
const userId = generateAdminUserHash();
const reqParams = { const reqParams = {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
event, event,
uuid, userId,
deviceId, deviceId,
properties: stringifyDeep({ eventProperties: payload.eventProperties,
...payload, userProperties: userId ? { ...anonymousUserProperties, ...payload.userProperties } : {},
...anonymousMetadata, groupProperties: {
}), ...anonymousGroupProperties,
...payload.groupProperties,
},
}), }),
..._.merge({}, defaultQueryOpts, opts), ..._.merge({}, defaultQueryOpts, opts),
}; };
try { try {
const res = await fetch(`${ANALYTICS_URI}/track`, reqParams); const res = await fetch(`${ANALYTICS_URI}/api/v2/track`, reqParams);
return res.ok; return res.ok;
} catch (err) { } catch (err) {
return false; return false;

View File

@ -17,7 +17,7 @@ try {
process.env.npm_config_global === 'true' || process.env.npm_config_global === 'true' ||
JSON.parse(process.env.npm_config_argv).original.includes('global') JSON.parse(process.env.npm_config_argv).original.includes('global')
) { ) {
fetch('https://analytics.strapi.io/track', { fetch('https://analytics.strapi.io/api/v2/track', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
event: 'didInstallStrapi', event: 'didInstallStrapi',

View File

@ -40,10 +40,12 @@ module.exports = {
if (deletedFiles.length + deletedFolders.length > 1) { if (deletedFiles.length + deletedFolders.length > 1) {
strapi.telemetry.send('didBulkDeleteMediaLibraryElements', { strapi.telemetry.send('didBulkDeleteMediaLibraryElements', {
rootFolderNumber: deletedFolders.length, eventProperties: {
rootAssetNumber: deletedFiles.length, rootFolderNumber: deletedFolders.length,
totalFolderNumber, rootAssetNumber: deletedFiles.length,
totalAssetNumber: totalFileNumber + deletedFiles.length, totalFolderNumber,
totalAssetNumber: totalFileNumber + deletedFiles.length,
},
}); });
} }
@ -229,10 +231,12 @@ module.exports = {
}); });
strapi.telemetry.send('didBulkMoveMediaLibraryElements', { strapi.telemetry.send('didBulkMoveMediaLibraryElements', {
rootFolderNumber: updatedFolders.length, eventProperties: {
rootAssetNumber: updatedFiles.length, rootFolderNumber: updatedFolders.length,
totalFolderNumber, rootAssetNumber: updatedFiles.length,
totalAssetNumber: totalFileNumber + updatedFiles.length, totalFolderNumber,
totalAssetNumber: totalFileNumber + updatedFiles.length,
},
}); });
ctx.body = { ctx.body = {

View File

@ -91,7 +91,9 @@ module.exports = ({ strapi }) => ({
async sendMetrics() { async sendMetrics() {
const metrics = await this.computeMetrics(); const metrics = await this.computeMetrics();
strapi.telemetry.send('didSendUploadPropertiesOnceAWeek', metrics); strapi.telemetry.send('didSendUploadPropertiesOnceAWeek', {
groupProperties: { metrics },
});
const metricsInfoStored = await getMetricsStoreValue(); const metricsInfoStored = await getMetricsStoreValue();
await setMetricsStoreValue({ ...metricsInfoStored, lastWeeklyUpdate: new Date().getTime() }); await setMetricsStoreValue({ ...metricsInfoStored, lastWeeklyUpdate: new Date().getTime() });

View File

@ -340,6 +340,7 @@ module.exports = ({ strapi }) => ({
if (user) { if (user) {
fileValues[UPDATED_BY_ATTRIBUTE] = user.id; fileValues[UPDATED_BY_ATTRIBUTE] = user.id;
} }
sendMediaMetrics(fileValues); sendMediaMetrics(fileValues);
const res = await strapi.entityService.update(FILE_MODEL_UID, id, { data: fileValues }); const res = await strapi.entityService.update(FILE_MODEL_UID, id, { data: fileValues });
@ -355,6 +356,7 @@ module.exports = ({ strapi }) => ({
fileValues[UPDATED_BY_ATTRIBUTE] = user.id; fileValues[UPDATED_BY_ATTRIBUTE] = user.id;
fileValues[CREATED_BY_ATTRIBUTE] = user.id; fileValues[CREATED_BY_ATTRIBUTE] = user.id;
} }
sendMediaMetrics(fileValues); sendMediaMetrics(fileValues);
const res = await strapi.query(FILE_MODEL_UID).create({ data: fileValues }); const res = await strapi.query(FILE_MODEL_UID).create({ data: fileValues });

View File

@ -53,33 +53,46 @@ function captureStderr(name, error) {
return captureError(name); return captureError(name);
} }
const getProperties = (scope, error) => ({ const getProperties = (scope, error) => {
error: typeof error === 'string' ? error : error && error.message, const eventProperties = {
os: os.type(), error: typeof error === 'string' ? error : error && error.message,
osPlatform: os.platform(), };
osArch: os.arch(), const userProperties = {
osRelease: os.release(), os: os.type(),
version: scope.strapiVersion, osPlatform: os.platform(),
nodeVersion: process.versions.node, osArch: os.arch(),
docker: scope.docker, osRelease: os.release(),
useYarn: scope.useYarn, nodeVersion: process.versions.node,
useTypescriptOnServer: scope.useTypescript, };
useTypescriptOnAdmin: scope.useTypescript, const groupProperties = {
isHostedOnStrapiCloud: process.env.STRAPI_HOSTING === 'strapi.cloud', version: scope.strapiVersion,
noRun: (scope.runQuickstartApp !== true).toString(), docker: scope.docker,
}); useYarn: scope.useYarn,
useTypescriptOnServer: scope.useTypescript,
useTypescriptOnAdmin: scope.useTypescript,
isHostedOnStrapiCloud: process.env.STRAPI_HOSTING === 'strapi.cloud',
noRun: (scope.runQuickstartApp !== true).toString(),
projectId: scope.uuid,
};
function trackEvent(event, body) { return {
eventProperties,
userProperties,
groupProperties: addPackageJsonStrapiMetadata(groupProperties, scope),
};
};
function trackEvent(event, payload) {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
return; return;
} }
try { try {
return fetch('https://analytics.strapi.io/track', { return fetch('https://analytics.strapi.io/api/v2/track', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
event, event,
...body, ...payload,
}), }),
timeout: 1000, timeout: 1000,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -91,14 +104,12 @@ function trackEvent(event, body) {
} }
function trackError({ scope, error }) { function trackError({ scope, error }) {
const { uuid } = scope;
const properties = getProperties(scope, error); const properties = getProperties(scope, error);
try { try {
return trackEvent('didNotCreateProject', { return trackEvent('didNotCreateProject', {
uuid,
deviceId: scope.deviceId, deviceId: scope.deviceId,
properties: addPackageJsonStrapiMetadata(properties, scope), ...properties,
}); });
} catch (err) { } catch (err) {
/** ignore errors */ /** ignore errors */
@ -107,14 +118,12 @@ function trackError({ scope, error }) {
} }
function trackUsage({ event, scope, error }) { function trackUsage({ event, scope, error }) {
const { uuid } = scope;
const properties = getProperties(scope, error); const properties = getProperties(scope, error);
try { try {
return trackEvent(event, { return trackEvent(event, {
uuid,
deviceId: scope.deviceId, deviceId: scope.deviceId,
properties: addPackageJsonStrapiMetadata(properties, scope), ...properties,
}); });
} catch (err) { } catch (err) {
/** ignore errors */ /** ignore errors */

View File

@ -44,7 +44,9 @@ describe('Metrics', () => {
await sendDidInitializeEvent(); await sendDidInitializeEvent();
expect(strapi.telemetry.send).toHaveBeenCalledWith('didInitializeI18n', { expect(strapi.telemetry.send).toHaveBeenCalledWith('didInitializeI18n', {
numberOfContentTypes: 1, groupProperties: {
numberOfContentTypes: 1,
},
}); });
}); });
@ -88,7 +90,9 @@ describe('Metrics', () => {
await sendDidUpdateI18nLocalesEvent(); await sendDidUpdateI18nLocalesEvent();
expect(strapi.telemetry.send).toHaveBeenCalledWith('didUpdateI18nLocales', { expect(strapi.telemetry.send).toHaveBeenCalledWith('didUpdateI18nLocales', {
numberOfLocales: 3, groupProperties: {
numberOfLocales: 3,
},
}); });
}); });
}); });

View File

@ -11,13 +11,15 @@ const sendDidInitializeEvent = async () => {
0 0
)(strapi.contentTypes); )(strapi.contentTypes);
await strapi.telemetry.send('didInitializeI18n', { numberOfContentTypes }); await strapi.telemetry.send('didInitializeI18n', { groupProperties: { numberOfContentTypes } });
}; };
const sendDidUpdateI18nLocalesEvent = async () => { const sendDidUpdateI18nLocalesEvent = async () => {
const numberOfLocales = await getService('locales').count(); const numberOfLocales = await getService('locales').count();
await strapi.telemetry.send('didUpdateI18nLocales', { numberOfLocales }); await strapi.telemetry.send('didUpdateI18nLocales', {
groupProperties: { numberOfLocales },
});
}; };
module.exports = () => ({ module.exports = () => ({