Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
f2e80a6d
Commit
f2e80a6d
authored
Apr 03, 2024
by
Alejandro Celaya
Committed by
Alejandro Celaya
Apr 03, 2024
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Track every time pending updates are applied
parent
d3f07781
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
153 additions
and
9 deletions
+153
-9
PendingUpdatesButton.tsx
src/sidebar/components/PendingUpdatesButton.tsx
+8
-4
PendingUpdatesNotification.tsx
src/sidebar/components/PendingUpdatesNotification.tsx
+11
-5
PendingUpdatesButton-test.js
src/sidebar/components/test/PendingUpdatesButton-test.js
+13
-0
PendingUpdatesNotification-test.js
...idebar/components/test/PendingUpdatesNotification-test.js
+12
-0
index.tsx
src/sidebar/index.tsx
+2
-0
analytics.ts
src/sidebar/services/analytics.ts
+18
-0
api.ts
src/sidebar/services/api.ts
+19
-0
analytics-test.js
src/sidebar/services/test/analytics-test.js
+53
-0
api-index.json
src/sidebar/services/test/api-index.json
+9
-0
api-test.js
src/sidebar/services/test/api-test.js
+8
-0
No files found.
src/sidebar/components/PendingUpdatesButton.tsx
View file @
f2e80a6d
...
...
@@ -3,27 +3,30 @@ import { useCallback, useEffect } from 'preact/hooks';
import
{
useShortcut
}
from
'../../shared/shortcut'
;
import
{
withServices
}
from
'../service-context'
;
import
type
{
AnalyticsService
}
from
'../services/analytics'
;
import
type
{
StreamerService
}
from
'../services/streamer'
;
import
type
{
ToastMessengerService
}
from
'../services/toast-messenger'
;
import
{
useSidebarStore
}
from
'../store'
;
export
type
PendingUpdatesButtonProps
=
{
// Injected
analytics
:
AnalyticsService
;
streamer
:
StreamerService
;
toastMessenger
:
ToastMessengerService
;
};
function
PendingUpdatesButton
({
analytics
,
streamer
,
toastMessenger
,
}:
PendingUpdatesButtonProps
)
{
const
store
=
useSidebarStore
();
const
pendingUpdateCount
=
store
.
pendingUpdateCount
();
const
hasPendingUpdates
=
store
.
hasPendingUpdates
();
const
applyPendingUpdates
=
useCallback
(
()
=>
streamer
.
applyPendingUpdates
(),
[
streamer
],
);
const
applyPendingUpdates
=
useCallback
(
()
=>
{
streamer
.
applyPendingUpdates
();
analytics
.
trackEvent
(
'client.realtime.apply_updates'
);
},
[
analytics
,
streamer
]
);
useShortcut
(
'l'
,
()
=>
hasPendingUpdates
&&
applyPendingUpdates
());
...
...
@@ -57,6 +60,7 @@ function PendingUpdatesButton({
}
export
default
withServices
(
PendingUpdatesButton
,
[
'analytics'
,
'streamer'
,
'toastMessenger'
,
]);
src/sidebar/components/PendingUpdatesNotification.tsx
View file @
f2e80a6d
...
...
@@ -6,12 +6,14 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import
{
pluralize
}
from
'../../shared/pluralize'
;
import
{
useShortcut
}
from
'../../shared/shortcut'
;
import
{
withServices
}
from
'../service-context'
;
import
type
{
AnalyticsService
}
from
'../services/analytics'
;
import
type
{
StreamerService
}
from
'../services/streamer'
;
import
{
useSidebarStore
}
from
'../store'
;
export
type
PendingUpdatesNotificationProps
=
{
// Injected
streamer
:
StreamerService
;
analytics
:
AnalyticsService
;
// Test seams
setTimeout_
?:
typeof
setTimeout
;
...
...
@@ -43,6 +45,7 @@ const collapseDelay = 5000;
function
PendingUpdatesNotification
({
streamer
,
analytics
,
/* istanbul ignore next - test seam */
setTimeout_
=
setTimeout
,
/* istanbul ignore next - test seam */
...
...
@@ -51,10 +54,10 @@ function PendingUpdatesNotification({
const
store
=
useSidebarStore
();
const
pendingUpdateCount
=
store
.
pendingUpdateCount
();
const
hasPendingChanges
=
store
.
hasPendingUpdatesOrDeletions
();
const
applyPendingUpdates
=
useCallback
(
()
=>
streamer
.
applyPendingUpdates
(),
[
streamer
],
);
const
applyPendingUpdates
=
useCallback
(
()
=>
{
streamer
.
applyPendingUpdates
();
analytics
.
trackEvent
(
'client.realtime.apply_updates'
);
},
[
analytics
,
streamer
]
);
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
timeout
=
useRef
<
number
|
null
>
(
null
);
...
...
@@ -110,4 +113,7 @@ function PendingUpdatesNotification({
);
}
export
default
withServices
(
PendingUpdatesNotification
,
[
'streamer'
]);
export
default
withServices
(
PendingUpdatesNotification
,
[
'streamer'
,
'analytics'
,
]);
src/sidebar/components/test/PendingUpdatesButton-test.js
View file @
f2e80a6d
...
...
@@ -6,6 +6,7 @@ describe('PendingUpdatesButton', () => {
let
fakeToastMessenger
;
let
fakeStore
;
let
fakeStreamer
;
let
fakeAnalytics
;
beforeEach
(()
=>
{
fakeToastMessenger
=
{
...
...
@@ -18,6 +19,9 @@ describe('PendingUpdatesButton', () => {
fakeStreamer
=
{
applyPendingUpdates
:
sinon
.
stub
(),
};
fakeAnalytics
=
{
trackEvent
:
sinon
.
stub
(),
};
$imports
.
$mock
({
'../store'
:
{
useSidebarStore
:
()
=>
fakeStore
},
...
...
@@ -36,6 +40,7 @@ describe('PendingUpdatesButton', () => {
<
PendingUpdatesButton
streamer
=
{
fakeStreamer
}
toastMessenger
=
{
fakeToastMessenger
}
analytics
=
{
fakeAnalytics
}
/>
,
);
};
...
...
@@ -82,6 +87,10 @@ describe('PendingUpdatesButton', () => {
applyBtn
.
props
().
onClick
();
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
calledWith
(
fakeAnalytics
.
trackEvent
,
'client.realtime.apply_updates'
,
);
});
it
(
'applies updates when keyboard shortcut is pressed'
,
()
=>
{
...
...
@@ -94,5 +103,9 @@ describe('PendingUpdatesButton', () => {
);
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
calledWith
(
fakeAnalytics
.
trackEvent
,
'client.realtime.apply_updates'
,
);
});
});
src/sidebar/components/test/PendingUpdatesNotification-test.js
View file @
f2e80a6d
...
...
@@ -10,6 +10,7 @@ describe('PendingUpdatesNotification', () => {
let
fakeSetTimeout
;
let
fakeClearTimeout
;
let
fakeStreamer
;
let
fakeAnalytics
;
let
fakeStore
;
beforeEach
(()
=>
{
...
...
@@ -18,6 +19,9 @@ describe('PendingUpdatesNotification', () => {
fakeStreamer
=
{
applyPendingUpdates
:
sinon
.
stub
(),
};
fakeAnalytics
=
{
trackEvent
:
sinon
.
stub
(),
};
fakeStore
=
{
pendingUpdateCount
:
sinon
.
stub
().
returns
(
3
),
hasPendingUpdatesOrDeletions
:
sinon
.
stub
().
returns
(
true
),
...
...
@@ -38,6 +42,7 @@ describe('PendingUpdatesNotification', () => {
return
mount
(
<
PendingUpdatesNotification
streamer
=
{
fakeStreamer
}
analytics
=
{
fakeAnalytics
}
setTimeout_
=
{
fakeSetTimeout
}
clearTimeout_
=
{
fakeClearTimeout
}
/>
,
...
...
@@ -106,8 +111,13 @@ describe('PendingUpdatesNotification', () => {
const
wrapper
=
createComponent
();
assert
.
notCalled
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
notCalled
(
fakeAnalytics
.
trackEvent
);
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
calledWith
(
fakeAnalytics
.
trackEvent
,
'client.realtime.apply_updates'
,
);
});
[
true
,
false
].
forEach
(
hasPendingUpdates
=>
{
...
...
@@ -121,6 +131,7 @@ describe('PendingUpdatesNotification', () => {
wrapper
=
createComponent
(
container
);
assert
.
notCalled
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
notCalled
(
fakeAnalytics
.
trackEvent
);
document
.
documentElement
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
key
:
'l'
}),
);
...
...
@@ -128,6 +139,7 @@ describe('PendingUpdatesNotification', () => {
fakeStreamer
.
applyPendingUpdates
.
called
,
hasPendingUpdates
,
);
assert
.
equal
(
fakeAnalytics
.
trackEvent
.
called
,
hasPendingUpdates
);
}
finally
{
wrapper
?.
unmount
();
container
.
remove
();
...
...
src/sidebar/index.tsx
View file @
f2e80a6d
...
...
@@ -17,6 +17,7 @@ import {
preStartServer
as
preStartRPCServer
,
}
from
'./cross-origin-rpc'
;
import
{
ServiceContext
}
from
'./service-context'
;
import
{
AnalyticsService
}
from
'./services/analytics'
;
import
{
AnnotationActivityService
}
from
'./services/annotation-activity'
;
import
{
AnnotationsService
}
from
'./services/annotations'
;
import
{
AnnotationsExporter
}
from
'./services/annotations-exporter'
;
...
...
@@ -134,6 +135,7 @@ function startApp(settings: SidebarSettings, appEl: HTMLElement) {
// Register services.
container
.
register
(
'analytics'
,
AnalyticsService
)
.
register
(
'annotationsExporter'
,
AnnotationsExporter
)
.
register
(
'annotationsService'
,
AnnotationsService
)
.
register
(
'annotationActivity'
,
AnnotationActivityService
)
...
...
src/sidebar/services/analytics.ts
0 → 100644
View file @
f2e80a6d
import
type
{
AnalyticsEventName
,
APIService
}
from
'./api'
;
/**
* @inject
*/
export
class
AnalyticsService
{
private
_api
:
APIService
;
constructor
(
api
:
APIService
)
{
this
.
_api
=
api
;
}
trackEvent
(
name
:
AnalyticsEventName
):
void
{
this
.
_api
.
analytics
.
events
.
create
({},
{
event
:
name
})
.
catch
(
e
=>
console
.
warn
(
`Could not track event "
${
name
}
"`
,
e
));
}
}
src/sidebar/services/api.ts
View file @
f2e80a6d
...
...
@@ -171,6 +171,12 @@ type ListGroupParams = {
expand
?:
string
[];
};
export
type
AnalyticsEventName
=
'client.realtime.apply_updates'
;
export
type
AnalyticsEvent
=
{
event
:
AnalyticsEventName
;
};
/**
* API client for the Hypothesis REST API.
*
...
...
@@ -224,6 +230,11 @@ export class APIService {
read
:
APICall
<
{
authority
?:
string
},
void
,
Profile
>
;
update
:
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Profile
>
,
Profile
>
;
};
analytics
:
{
events
:
{
create
:
APICall
<
Record
<
string
,
unknown
>
,
AnalyticsEvent
>
;
};
};
constructor
(
apiRoutes
:
APIRoutesService
,
...
...
@@ -304,6 +315,14 @@ export class APIService {
Profile
>
,
};
this
.
analytics
=
{
events
:
{
create
:
apiCall
(
'analytics.events.create'
)
as
APICall
<
Record
<
string
,
unknown
>
,
AnalyticsEvent
>
,
},
};
}
/**
...
...
src/sidebar/services/test/analytics-test.js
0 → 100644
View file @
f2e80a6d
import
sinon
from
'sinon'
;
import
{
AnalyticsService
}
from
'../analytics'
;
describe
(
'AnalyticsService'
,
()
=>
{
let
fakeAPIService
;
let
analyticsService
;
beforeEach
(()
=>
{
fakeAPIService
=
{
analytics
:
{
events
:
{
create
:
sinon
.
stub
().
resolves
(),
},
},
};
analyticsService
=
new
AnalyticsService
(
fakeAPIService
);
});
describe
(
'trackEvent'
,
()
=>
{
it
(
'creates an event through the API'
,
()
=>
{
analyticsService
.
trackEvent
(
'client.realtime.apply_updates'
);
assert
.
calledWith
(
fakeAPIService
.
analytics
.
events
.
create
,
{},
{
event
:
'client.realtime.apply_updates'
},
);
});
it
(
'logs '
,
async
()
=>
{
const
error
=
new
Error
(
'something failed'
);
fakeAPIService
.
analytics
.
events
.
create
.
rejects
(
error
);
sinon
.
stub
(
console
,
'warn'
);
try
{
analyticsService
.
trackEvent
(
'client.realtime.apply_updates'
);
// Wait for next tick so that the API call promise settles
await
Promise
.
resolve
();
assert
.
calledWith
(
console
.
warn
,
'Could not track event "client.realtime.apply_updates"'
,
error
,
);
}
finally
{
console
.
warn
.
restore
();
}
});
});
});
src/sidebar/services/test/api-index.json
View file @
f2e80a6d
...
...
@@ -84,6 +84,15 @@
"method"
:
"DELETE"
,
"desc"
:
"Delete an annotation"
}
},
"analytics"
:
{
"events"
:
{
"create"
:
{
"url"
:
"https://example.com/api/analytics/events"
,
"method"
:
"POST"
,
"desc"
:
"Create a new analytics event"
}
}
}
}
}
src/sidebar/services/test/api-test.js
View file @
f2e80a6d
...
...
@@ -196,6 +196,14 @@ describe('APIService', () => {
return
api
.
profile
.
update
({},
{
preferences
:
{}
});
});
it
(
'creates analytics event'
,
()
=>
{
expectCall
(
'post'
,
'analytics/events'
);
return
api
.
analytics
.
events
.
create
(
{},
{
event
:
'client.realtime.apply_updates'
},
);
});
context
(
'when an API call fails'
,
()
=>
{
[
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment