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

Offline storage plugin #1165

Closed
wants to merge 2 commits into from

Conversation

seromenho
Copy link

This is my proposal to #279. Any thoughts/suggestions to improve or make it different are welcome.
Thanks.
(#973 continues here.)

Description

Stores errors failed to get send and try to send them when network comes back or on init

Options

  • onlineEventName: In case you have a custom event fired instead of online you can pass it here.

TODO

If this works for you I'm missing documentation and tests.

Stores errors failed to get send and try to send them when
Network comes back or on init

var offlineStorageKey = 'raven-js-offline-queue';

function offlineStoragelugin(Raven, options) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Type in offlineStoragelugin

window.addEventListener(options.onlineEventName || 'online', processOfflineQueue);
}

module.exports = offlineStoragelugin;
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo here as well

Copy link
Author

Choose a reason for hiding this comment

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

Thanks

var queue = JSON.parse(localStorage.getItem(offlineStorageKey)) || [];

// Store an empty queue. If processing these one fails they get back to the queue
localStorage.setItem(offlineStorageKey, JSON.stringify([]));
Copy link
Contributor

Choose a reason for hiding this comment

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

We can call removeItem instead to be more direct. It'll fallback to [] anyway during processing

Copy link
Author

Choose a reason for hiding this comment

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

true :)

queue.forEach(function processOfflinePayload(data) {
// Avoid duplication verification for offline stored
// as they may try multiple times to be processed
Raven._lastData = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if I understand your comment here.
There shouldn't be any duplicates here already, as failure event is triggered at the end of _sendProcessedPayload which already checks for duplicates first.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, but then when trying to send it again when get back offline, because _lastData is set with potentially the same data we will be try to send, it will be missed because it's the same data.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right :)


try {
var queue = JSON.parse(localStorage.getItem(offlineStorageKey)) || [];
queue.push(event.data || Raven._lastData);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the purpose of falling back to _lastData?

Copy link
Author

Choose a reason for hiding this comment

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

Just to make sure, from what I have seen they will have(and be) to be the same right?
I can remove the fallback.

});

// Add event listener on online or custom event to trigger offline queue sending
window.addEventListener(options.onlineEventName || 'online', processOfflineQueue);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any alternative event that I don't know of, but could be used here? :P

Copy link
Author

Choose a reason for hiding this comment

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

This is just because the application might rely on a different event for letting other plugins know when the connection is available (e.g. on our cordova-based hybrid app we trigger a "foo-bar-online" event to the document when we're sure the app is online and it was able to make an XHR request), because "online" can easily mean "connected to the network" as in "local network" but still not be connected to the internet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes perfect sense

@seromenho
Copy link
Author

@kamilogorek Thank you som much for your review!
I have implemented your requested changes.
Let me know if it's all good now.

@kamilogorek
Copy link
Contributor

I'd say the implementation is fine now, thanks! :)
Would you mind adding some tests for the main functionality?

@seromenho
Copy link
Author

Great, thanks.
As far for the tests. I won't be able to add them at least on this week.
I'll try to add them when possible or I'll accept help from anyone 😄

@kamilogorek
Copy link
Contributor

No rush, take your time :)
Unfortunately, I have some other tasks to tackle, so I cannot help this week as well.

var queue = JSON.parse(localStorage.getItem(offlineStorageKey)) || [];

// Store an empty queue. If processing these one fails they get back to the queue
localStorage.removeItem(offlineStorageKey);

Choose a reason for hiding this comment

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

multiple localStorage operations are not atomic. It is possible for multiple tabs to receive this queue before the removeItem. This needs to be inside a lock. See more info here:
https://medium.engineering/wait-dont-touch-that-a211832adc3a
http://balpha.de/2012/03/javascript-concurrency-and-locking-the-html5-localstorage/

Copy link
Contributor

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

@graingert I don't believe that library handles atomic operations between multiple browser windows. It just removes oldest item when localstorage when it's full

Choose a reason for hiding this comment

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

Here is a link to a library I created to solve this problem. There are probably other libraries out there as well. Also feel free to just copy the library code into this repo if you don't want another dependency.

https://github.com/taylorhakes/localstorage-lock/

Copy link

@jasekiw jasekiw Mar 16, 2018

Choose a reason for hiding this comment

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

Storing an entire array of the logs at once can be quite a poor usage of memory. There might be a lot of logs if a device is left offline for extended periods of time. On hybrid apps there is a very small memory limit. If there are a lot of logs or large json encoded objects, memory can spike. Possibly using indexdb or an option for a custom storage adapter would allow for more memory efficient ways of storing.

* Offline Storage plugin
*
* Stores errors failed to get send and try to send them when
* Networkf comes back or on init
Copy link
Contributor

Choose a reason for hiding this comment

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

(sorry, this is a drive-by comment)

is this a typo, Networkf? should be Network, right?

/**
* Offline Storage plugin
*
* Stores errors failed to get send and try to send them when
Copy link
Contributor

Choose a reason for hiding this comment

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

should failed to get send be that failed to be sent?

@kamilogorek
Copy link
Contributor

Not relevant for sentry-javascript SDK v4.

@kamilogorek kamilogorek closed this Aug 2, 2018
@AgDude
Copy link

AgDude commented Aug 2, 2018

@kamilogorek I am excited to hear that apparently raven will support offline natively. Any estimate on when we can expect to see v4?

@kamilogorek
Copy link
Contributor

@AgDude Sentry core and node packages are already released as beta. We also merged major browser sdk rewrite (https://github.com/getsentry/sentry-javascript/commits/master quite a lot of commits :p) Today and now in a process of cleaning things outs.

v4 will be extensible in a lot of ways, therefore adding offline integration (we'll write it ourselves anyway) will be a breeze :)

It should be released as a major version early Q3, around September/October.

@AgDude
Copy link

AgDude commented Aug 2, 2018

Sounds great! Thanks for the update.

@seromenho
Copy link
Author

Awesome! Thanks

@joelryan2k
Copy link

@kamilogorek We too are interested in capturing errors offline. Any update on this?

If someone can provide general direction on the recommended approach I have time to work on this... or test a work in progress. I see from your August 2nd comment that it appears this can be built as an Sentry 4 "Integration"?

@kamilogorek
Copy link
Contributor

kamilogorek commented Oct 8, 2018

@joelryan2k veeeery rough sketch of what it can look like. We'll definitely release an official integration, but we still have a lot of things to polish first.

import { configureScope, captureEvent } from '@sentry/browser';
import { serialize, deserialize } from '@sentry/utils/object';

class Offline {
  constructor() {
    // initialize storage
    this.storage = someApi;
    // get items
    this.queue = this.storage.getEvents();
    // drain them
    this.drainQueue(this.queue);
  }
  
  install() {
    configureScope(scope => scope.addEventProcessor(async (event) => {
      if (isOnline()) {
        return event;
      }
      await this.storeEvent(event);
      return event;
    }))
  }
  
  storeEvent(event) {
    return this.storage.storeEvent(serialize(event));
  }
  
  drainQueue(queue) {
    // send each one to sentry
    queue.forEach((event) => captureEvent(deserialize(event)));
  }
}

@kamilogorek kamilogorek mentioned this pull request Oct 9, 2018
@kamilogorek
Copy link
Contributor

Created #1633 to keep a track of this feature

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.

None yet

9 participants