Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration for offline support #2778

Merged
merged 15 commits into from
Aug 25, 2020

Conversation

davidmyersdev
Copy link
Contributor

@davidmyersdev davidmyersdev commented Jul 28, 2020

Before submitting a pull request, please take a look at our
Contributing guidelines and verify:

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).

Summary

Cache offline events to send when connectivity is reestablished.

This PR is an adaptation of #2216. Resolves #1633.

@davidmyersdev davidmyersdev force-pushed the add-offline-support branch 3 times, most recently from 18c37fb to b0d0260 Compare July 28, 2020 04:09
@davidmyersdev davidmyersdev marked this pull request as draft July 28, 2020 15:07
@davidmyersdev davidmyersdev changed the title WIP: Add integration for offline support Add integration for offline support Jul 28, 2020
Offline events are cached in the browser with localforage.
@davidmyersdev
Copy link
Contributor Author

Okay, I think I have this in a good place now, but I still need to write some tests. The final failing check is related to bundle size, so I'm not entirely sure how to proceed. I assume this is from the addition of the localforage dependency.

@AbhiPrasad
Copy link
Member

AbhiPrasad commented Jul 29, 2020

This looks good at an initial glance, but before we do a deeper review, @voraciousdev do you mind if you could move this integration into our integrations package? I think this is the best place for features like this.

https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations.

Also do you have a demo repo or something of the sort you've been testing this with? Would help with review/testing on our side (also to explore all the use cases).

Thanks for taking this on, the Sentry team appreciates it ❤️

As an aside it seems that the bundle check is failing because you are trying to merge from a fork? Don't think it is your problem, if you look all the size checks still pass.

Error creating comment. This can happen for PR's originating from a fork without write permissions.

We will investigate this on our side.

@AbhiPrasad AbhiPrasad self-requested a review July 29, 2020 14:32
I thought this build was passing, but it looks like there is not an easy way to use a standard import for localforage in this library.
@davidmyersdev
Copy link
Contributor Author

@AbhiPrasad I appreciate the feedback! I believe I moved this to the correct place now. The repo I've been using to test this is closed source, but I'll see if I can throw an example together today.

Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more thoughts I had.

Once you get around to the tests, I'll do a more involved review (and clone the branch locally for some local testing).

packages/integrations/package.json Outdated Show resolved Hide resolved
packages/integrations/src/offline.ts Outdated Show resolved Hide resolved
packages/integrations/src/offline.ts Outdated Show resolved Hide resolved
* @inheritDoc
*/
public constructor() {
this.offlineEventStore = localforage.createInstance({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could createInstance fail? What happens to this.offlineEventStore then?

We can import localforage typings if they exist as well.

Also, what browsers does localforage support? It's fine if it doesn't match Sentry's (https://docs.sentry.io/platforms/javascript/#browser-table) - we just have to document it then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch. I meant to wrap this in a try/catch. I will look into importing the type file too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it meets the compatibility requirements. It is a wrapper around multiple storage mechanisms. It attempts to use IndexedDB, then falls back to WebSQL, and if all else fails, it falls back to localStorage.
https://github.com/localForage/localForage/wiki/Supported-Browsers-Platforms

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice ok, makes sense. Just one last thing, what happens when local storage is full? Is it a queue, or do new events just get dropped?

@bruno-garcia, how do we handle offline queue in mobile right now?

Copy link
Member

@bruno-garcia bruno-garcia Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only envelopes are cached (no event json alone are cached anymore is what I mean). The order they were cached is kept when attempting to send things to Sentry (i.e device is back online). If maxItems is reached (by default 30 IIRC), the oldest file is deleted to make space to the newest one.

We don't use dependencies or a DB of any sort. It's just files written to a directory.

If session data is stored (release health), the init=true flag must be preserved. So when deleting a file to make room for a new one (max items reached), the envelope would need to be unwrapped, checked for sessions and if init=true exists, that must be moved to the next session update queued up for submission (from oldest to newest).
Events capture when session tracking is on must be in an envelope together with a session update.

Note that the order being kept means that if you get a call to captureException, you won't be sending that to Sentry if there's stuff cached. You must prioritize what's on the storage first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Bruno for coming through 🚀. We can ignore all the sessions stuff, and envelope details - although important for me, probably not as for you @voraciousdev

Basically there are two things we need to keep in mind.

  1. Enforce some kind of maxItems in local storage. This can be changed through an option.

  2. Make sure that we send all events from local storage first before we continue on with other events. I know we are using an event listener right now to listen to online, do you think that is good enough? Maybe we need another check in the globalEventProcessor to be sure.

As for how this will work with sessions, we can get to that when release health comes to JS 😄

Copy link
Contributor Author

@davidmyersdev davidmyersdev Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AbhiPrasad the LocalForage.setItem promise will be rejected if there is not enough space on the device. With the way it's currently written in this integration, that would result in logger.warn('could not cache event while offline') being called.

Copy link
Member

@AbhiPrasad AbhiPrasad Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok perfect we should be good then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that we send all events from local storage first before we continue on with other events. I know we are using an event listener right now to listen to online, do you think that is good enough? Maybe we need another check in the globalEventProcessor to be sure.

@AbhiPrasad I'm not sure if this is directed at me, but there is definitely a possible race condition here regarding the online event. Given the nature of this integration (to support offline-capable apps), I think it's a real scenario that we could have real error events triggered as soon as the online event is fired. For offline apps, it's common practice to listen to the online event and run some code (which could then lead to an error being thrown/recorded).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added localforage typings as well. Good call on that. 👍

@davidmyersdev
Copy link
Contributor Author

@AbhiPrasad I believe I have addressed all open comments, and I just added tests. Mind taking another look and letting me know if there's anything else you need me to change? Thanks!

@davidmyersdev davidmyersdev marked this pull request as ready for review August 3, 2020 01:17
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice LGTM, thanks for sticking with it. Just before approval, I would like to get a double check from @HazAT to see if there is anything else left (also to see if this might cause any issues with rate limiting we have to concern ourselves with). Once he gives to OK, I will approve and merge.

Also, I thought about the race condition a little bit more, but I think it's still fine to listen on online. We can come back to this if many people report a problem.

@AbhiPrasad AbhiPrasad requested a review from HazAT August 3, 2020 19:56
@davidmyersdev
Copy link
Contributor Author

@AbhiPrasad I did some more testing in my own project, and I discovered a couple of small issues that I believe are now fixed via the extra commits you see here. To break them down:

  • Fixing lockfile after migrating to @sentry/integrations
    • It turns out I had some lockfile changes that were committed when this was initially added to the @sentry/browser package.
  • Add TS compiler flag to allow import of localforage
    • The import assignment was not working properly in my es6/webpack project. It looks like some of the builds were failing. After some more debugging, it looks like using a standard import (e.g. import localforage from 'localforage') actually does work. It just throws a typescript error due to the atypical nature of the package exports.
  • Handle scenario where app has stored events from previous offline session
    • This is just a new scenario that I didn't previously think about. When an app is offline, just listening to the online event is not good enough. If the user navigates away from the app while offline and then navigates back while online, the online event would never be fired. Because of this, we will want to check for connectivity during the setupOnce call and send any previously stored events.

Copy link
Member

@HazAT HazAT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all thanks for this, this is awesome a lot of users already requested this 🥇
Generally, it looks good.

I have one additional ask, please add an option for max stored offline events. I see that localForage has a limit but I would like to have more control over this.

So for example, maxStoredEvents: 30 and after we reach more than 30, it acts like a ring buffer dropping the oldest events and replaces it with new ones coming in.

We had troubles with other SDKs already where this was more or less unbound so we need to add a reasonable limit by default.

@davidmyersdev
Copy link
Contributor Author

@HazAT thank you for following up! I can definitely manage that. I'll tag you for another review once it's ready.

@davidmyersdev davidmyersdev requested a review from HazAT August 22, 2020 17:54
@davidmyersdev
Copy link
Contributor Author

@HazAT I finally got around to adding the maxStoredEvents config (with a default limit of 30 as suggested). Let me know if there's anything else you need from me!

Copy link
Member

@HazAT HazAT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @voraciousdev thanks for keeping the ball rolling, aside from the merge conflicts I think we are good to go :)

packages/integrations/package.json Outdated Show resolved Hide resolved
@davidmyersdev
Copy link
Contributor Author

@HazAT it looks like there were some lint rules that changed since I started this. I will get it updated.

@davidmyersdev
Copy link
Contributor Author

I don't usually like merge commits, but the GitHub UI created one automatically when I resolved those conflicts. Let me know if you would prefer a rebase.

@davidmyersdev davidmyersdev requested a review from HazAT August 24, 2020 16:27
Copy link
Member

@HazAT HazAT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We squash commit anyway :)
Thanks @voraciousdev contribution of the year 🥇

@HazAT HazAT merged commit 8fbffe2 into getsentry:master Aug 25, 2020
@davidmyersdev
Copy link
Contributor Author

@HazAT any idea when this will be released?

@kamilogorek
Copy link
Contributor

@voraciousdev tomorrow :)

if (this.hub) {
const newEventId = this.hub.captureEvent(event);

if (newEventId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Hub.captureEvent method always returns new event ID, so event will always be purged from event store (even that client failed to send it).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #2890

name: 'sentry/offlineEventStore',
});

if ('addEventListener' in this.global) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event listener should be set up after hub is set on instance (in setupOnce method) as event may be fired before hub is set (hub is used in _sendEvents).

IMHO it's bad practice to have business logic in constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #2889

PeloWriter pushed a commit to getsentry/sentry-docs that referenced this pull request Mar 5, 2021
* Add documentation for Offline integration

See getsentry/sentry-javascript#2778

Remove backticks from plugin.mdx's description; PageGrid doesn't render them as monospace, so they appear as literals in https://docs.sentry.io/platforms/javascript/configuration/integrations/, and the default.mdx description doesn't use them for its class names.

* Apply suggestions from code review

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Offline Integration
6 participants