Manually invoke LMSCommit() on slide load with JS?

Hi,

I'd like to add "save points" to various slides within my course to ensure that learner progress is preserved as best as possible (e.g. guarding against a lost internet connection, failed race condition onwindowclose(), etc).

I see that I can execute arbitrary javascript using a trigger, and even that I can execute JS on "timeline start".  I tried setting this up and setting this:

LMSCommit()

...as the Javascript I want to execute, but nothing seemed to happen.  

Similarly, I'd like to add an "exit course" button to each of my slides, and again, I'd like the trigger on this button to do an LMSCommit back to the LMS, and *then* "window.close()" (I always open my courses in their own window).

Is there a way to do what I want?  I'm actually not 100% sure if "LMSCommit()" is even the JS command I want--what I want to do is send a "save the current state of the course; whatever that is" command.  My assumption is that LMSCommit() is that command.

Any help is appreciated.


Thanks,

Ben

40 Replies
Steve Flowers

The course itself will take care of submission and restore on the LMS as long as you publish to SCORM. No need to manually make those calls, it's taken care of behind the scenes. If you add an Exit Course trigger to a button, it'll close up the session and attempt to close the window.

There are some tricks if you want to run your own logic or manually set a score, but for the most part Storyline will take care of LMS stuff for you

Ben Mueller

Hi Steve,

Thanks for the reply.  I don't want to leave it up to the course.  I want more save points than what Storyline courses currently offer.  IMHO, Storyline's LMS support is pretty weak, and I'm trying to make it a little better, if I can.

Best I can figure, a Storyline course does an LMSCommit() when the course is first launched, and then when it's closed.  That's not enough, and that's really not reliable since a course may be closed by closing the browser window, which results in a race condition (e.g. "does the Exit Course command make it back to the LMS before the window is destroyed by the LMS?"  Sometimes yes, sometimes no).

Also, I believe there's a difference between "Exit Course" and "Save course state back to the LMS".  I'm talking only about the latter case, not the former.

So, again what I'm wondering is whether I can manually trigger just a "save current course state back to the LMS, but don't close the course" command using a trigger and the option to invoke Javascript.


Thanks again,

Ben

Steve Flowers

Here's a log from a multi-slide test. You'll see that it's sending suspend data updates for every slide change.

I also closed by closing the window. The exit handler is processing the session dusting and commits. I've never seen the race conditions you're talking about, not denying they exist - just never seen it happen in 10 years of working with LMSs. Most of my courses close using window.top.close(). Always just works.

+ [16:05:20.253] LMSInitialize('') returned 'true' in 0.001 seconds
+ [16:05:20.254] LMSGetValue('cmi.core.lesson_mode') returned 'normal' in 0.001 seconds
    [16:05:20.255] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:20.255] LMSGetValue('cmi.core.lesson_mode') returned 'normal' in 0 seconds
    [16:05:20.255] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:20.256] LMSGetValue('cmi.core.lesson_status') returned 'not attempted' in 0 seconds
    [16:05:20.256] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:20.256] LMSSetValue('cmi.core.lesson_status', 'incomplete') returned 'true' in 0.004 seconds
+ [16:05:20.260] LMSSetValue('cmi.core.exit', 'suspend') returned 'true' in 0 seconds
    [16:05:20.411] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_1.2_ORG
    [16:05:20.411] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_SCO
+ [16:05:20.500] LMSSetValue('cmi.core.session_time', '0000:00:00.23') returned 'true' in 0.001 seconds
+ [16:05:20.501] LMSCommit('') returned 'true' in 0 seconds
+ [16:05:20.502] LMSGetValue('cmi.suspend_data') returned '' in 0.001 seconds
    [16:05:20.503] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:20.503] LMSGetValue('cmi.core.lesson_status') returned 'incomplete' in 0 seconds
    [16:05:20.503] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:25.256] LMSSetValue('cmi.core.lesson_status', 'incomplete') returned 'true' in 0.002 seconds
+ [16:05:25.258] LMSSetValue('cmi.suspend_data', '') returned 'true' in 0 seconds
+ [16:05:25.258] LMSSetValue('cmi.core.session_time', '0000:00:04.99') returned 'true' in 0 seconds
+ [16:05:25.259] LMSCommit('') returned 'true' in 0 seconds
+ [16:05:26.789] LMSSetValue('cmi.suspend_data', '1V44050ji1001111a010110111100tn021ffe720118_default00000000') returned 'true' in 0 seconds
+ [16:05:29.748] LMSSetValue('cmi.suspend_data', '2F16405060on1001211f01011011110121100~231n021ffe720118_default000Ca9101001a1a3ZJ0h14e730118_default000100000') returned 'true' in 0 seconds
+ [16:05:31.117] LMSSetValue('cmi.suspend_data', '2W1840507060ts1001311k0101101111012110131100~2d1n021ffe720118_default000Ca9101001a1a3ZJ0h14e730118_default00010a02hl002100000') returned 'true' in 0 seconds
+ [16:05:33.296] LMSSetValue('cmi.suspend_data', '2D2a8040507060yx1001411p010110111101211013110141100~2P1n021ffe720118_default000Ca9101001a1a3ZJ0h14e730118_default00010a02hl002100Ca9101001a1a3Yx0h14e730118_default0001000') returned 'true' in 0.001 seconds
+ [16:05:33.483] LMSSetValue('cmi.core.lesson_status', 'completed') returned 'true' in 0.001 seconds
+ [16:05:33.484] LMSSetValue('cmi.suspend_data', '2U2c405070908060DC1001511u01011011110121101311014110151100~2Z1n021ffe720118_default000Ca9101001a1a3ZJ0h14e730118_default00010a02hl002100Ca9101001a1a3Yx0h14e730118_default00010a02sa0021000') returned 'true' in 0 seconds
+ [16:05:33.484] LMSSetValue('cmi.core.session_time', '0000:00:13.22') returned 'true' in 0.001 seconds
+ [16:05:33.485] LMSCommit('') returned 'true' in 0 seconds
+ [16:05:33.485] LMSSetValue('cmi.core.score.raw', '100') returned 'true' in 0.001 seconds
+ [16:05:33.486] LMSSetValue('cmi.core.score.max', '100') returned 'true' in 0 seconds
+ [16:05:33.486] LMSSetValue('cmi.core.score.min', '0') returned 'true' in 0 seconds
    [16:05:33.636] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_1.2_ORG
    [16:05:33.636] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_SCO
    [16:05:33.788] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_1.2_ORG
    [16:05:33.788] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_SCO
+ [16:05:33.980] LMSSetValue('cmi.suspend_data', '2U2c405070908060DC1001511u01011011110121101311014110151100~2Z1n021ffe720118_default000Ca9101001a1a3ZJ0h14e730118_default00010a02hl002100Ca9101001a1a3Yx0h14e730118_default00010a02sa0021000') returned 'true' in 0.001 seconds
+ [16:05:39.207] LMSSetValue('cmi.core.session_time', '0000:00:18.94') returned 'true' in 0 seconds
+ [16:05:39.208] LMSGetValue('cmi.core.lesson_mode') returned 'normal' in 0 seconds
    [16:05:39.208] LMSGetLastError() returned '0' in 0 seconds
+ [16:05:39.208] LMSSetValue('cmi.core.lesson_status', 'completed') returned 'true' in 0.001 seconds
+ [16:05:39.209] LMSSetValue('cmi.core.exit', '') returned 'true' in 0 seconds
+ [16:05:39.209] LMSCommit('') returned 'true' in 0 seconds
+ [16:05:39.209] LMSFinish('') returned 'true' in 0.002 seconds
Steve Flowers

Tried with 2 LMSs to get it to fail by torturing the launch windows (closing the main window before the module launch window). Still caught all SCORM calls. I'm not aware of any SCORM authoring tools that commit with each SetValue() call. Could exist but I've really never seen the need for it.

    [16:12:52.728] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_1.2_ORG
    [16:12:52.728] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_SCO
+ [16:12:52.999] LMSInitialize('') returned 'true' in 0 seconds
+ [16:12:52.999] LMSGetValue('cmi.core.lesson_mode') returned 'normal' in 0.001 seconds
    [16:12:53.0] LMSGetLastError() returned '0' in 0.001 seconds
+ [16:12:53.1] LMSGetValue('cmi.core.lesson_mode') returned 'normal' in 0 seconds
    [16:12:53.1] LMSGetLastError() returned '0' in 0 seconds
+ [16:12:53.1] LMSGetValue('cmi.core.lesson_status') returned 'not attempted' in 0 seconds
    [16:12:53.1] LMSGetLastError() returned '0' in 0 seconds
+ [16:12:53.2] LMSSetValue('cmi.core.lesson_status', 'incomplete') returned 'true' in 0.005 seconds
+ [16:12:53.7] LMSSetValue('cmi.core.exit', 'suspend') returned 'true' in 0.001 seconds
+ [16:12:53.28] LMSSetValue('cmi.core.session_time', '0000:00:00.1') returned 'true' in 0 seconds
+ [16:12:53.29] LMSCommit('') returned 'true' in 0 seconds
+ [16:12:53.30] LMSGetValue('cmi.suspend_data') returned '' in 0 seconds
    [16:12:53.30] LMSGetLastError() returned '0' in 0 seconds
+ [16:12:53.30] LMSGetValue('cmi.core.lesson_status') returned 'incomplete' in 0 seconds
    [16:12:53.30] LMSGetLastError() returned '0' in 0 seconds
    [16:12:53.180] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_1.2_ORG
    [16:12:53.181] Beginning prerequisites evaluation of activity averageScore_sendtoLMS_SCO
+ [16:12:53.436] LMSSetValue('cmi.core.lesson_status', 'incomplete') returned 'true' in 0.001 seconds
+ [16:12:53.437] LMSSetValue('cmi.suspend_data', '') returned 'true' in 0.001 seconds
+ [16:12:53.438] LMSSetValue('cmi.core.session_time', '0000:00:00.42') returned 'true' in 0 seconds
+ [16:12:53.438] LMSCommit('') returned 'true' in 0 seconds
+ [16:12:55.127] LMSSetValue('cmi.suspend_data', '1V44050ji1001111a010110111100tn02ehfe720118_default00000000') returned 'true' in 0 seconds
+ [16:12:57.753] LMSSetValue('cmi.suspend_data', '2F16405060on1001211f01011011110121100~231n02ehfe720118_default000Ca9101001a1a3IE0h14e730118_default000100000') returned 'true' in 0 seconds
+ [16:13:00.887] LMSSetValue('cmi.suspend_data', '2X1840507060ts1001311k0101101111012110131100~2e1n02ehfe720118_default000Ca9101001a1a3IE0h14e730118_default00010b03TM0002100000') returned 'true' in 0 seconds
+ [16:13:03.804] LMSSetValue('cmi.core.session_time', '0000:00:10.79') returned 'true' in 0 seconds
+ [16:13:03.804] LMSSetValue('cmi.core.exit', 'suspend') returned 'true' in 0 seconds
+ [16:13:03.805] LMSCommit('') returned 'true' in 0 seconds
+ [16:13:03.805] LMSFinish('')
+ [16:13:03.806] Pre-evaluation of exit action
+ [16:13:03.807] OverallSequencingProcess for SCORM 1.1 / SCORM 1.2
Phil Mayor

Steve knows much more than me on this, but I have never seen it in five years as an LMS admin. 

The only issues I ever have are with IE9 not communicating correctly, which is nothing to do with Storyline and would not be fixed by periodically saving.

we have over 3.000 users who accessed over 20,000 modules in six months. We never use an exit button and only rely on the close browser window. I don't see any difference in Storyline's LMS Support, to that of Captivate. 

Ben Mueller

So you're seeing a very different thing than I'm seeing.  What I see (OS X, Firefox, Firebug activated, course published to SCORM 2004 v2, completion determined by the # of slides viewed) is that the course fires off a save back to the server when the course is first launched (e.g. the first slide loads), and when the total # of slides needed for completion is reached, which is, in my case, the last slide of the course.  Firebug shows literally those two AJAX commands and those two only.

You're saying that on every slide load in your test, a "save current course state" command is being sent from the course back to the LMS?  That's actually too many saves for me, but I'd love to know how you're configuring Storyline to do that.  Also, how do you enter debug mode in Storyline?  I'd love to watch the chatter of my course as well.

Also, I'm not sure what LMS you're working with, but it sounds like a magical place with ice cream trees and fairy dust everywhere.  All my experience shows that window.close race conditions happen all the time.  (-; 

Steve Flowers

At any rate, if you really wanted to you could call LMSCommit(); on every slide load. No harm, I suppose. I wouldn't try to call it once ever second or anything like that. Too often and you're creating unnecessary requests. Doesn't sound like that's your goal.

JavaScript Trigger placed on your master slide when the timeline starts:

LMSCommit();

Steve

Steve Flowers

If you're looking at it with Firebug, make sure you're catching the LMS API calls running through Flash. It's a fairly complex assembly. I never look at SCORM calls through Firebug unless there's a problem. Usually trace through the LMS. There's certainly lots more than 2 as you see above in the simple example. Tons of stuff getting fired off throughout the course. No special configuration of SL to make that happen. Publish to SCORM and it takes care of the behind the scenes stuff

Ben Mueller

Okay, adding the "LMSCommit()" call as a JS trigger on timeline start works, and not just in Firebug, but in my LMS as well.  I record the # of total saves per session, and as I pass by the slide with the save point, I see the total # increment by one.  So I'll probably just add an LMSCommit() command to the first slide of every scene, just to be safe.

But now I'm totally confused.  Unless we're talking past each other and are really seeing the same behavior, it sounds like you're getting something a lot different than I am.  Does your LMS really show a state save on each and every slide load of your Storyline course?  If not, then how often does the LMS see state save commands from your Storyline course?  The data dump you have above shows a few LMSCommit commands, but most of them are just of the course and client-side LMS layer talking back and forth, but not necessarily going back to the LMS itself.  I can't quite tell where new slides are loaded in the data.

You say that some of this traffic is handled by Flash in Storyline, but presumably all those Flash commands still bottom out in standard AJAX calls, right?  In which case, Firebug is going to capture that traffic, since it's just looking for communications between client and server.  I suppose it's possible that Storyline is talking to the server in ways that Firebug doesn't see, but then neither does my LMS.  Not trying to be difficult here--just trying to get the clearest understanding of how things are/should be working.

Ben Mueller

Right, okay, that's what I thought.  I haven't configured FireBug in any way other than opening it, and if you're saying the calls are the same, then I feel pretty comfortable that my LMS and FireBug are reporting all of the calls the course is sending.

Thanks for the tip about LMSCommit() on timeline start.  I think that should take care of most of my issues.

Scott Lindsey

Good morning!

This is a very interesting thread. Thank you for starting it and for all the comments so far.

I'm wondering if this would help prevent timeouts with our LMS (Mzinga). Our system times out with 30 minutes of inactivity. If a learner is going through the slides, will my Storyline SCORM course make calls back to the LMS often enough to keep the session active? We're working on a course that is around an hour long. It doesn't have to be completed in one session but I believe people will do that and we'd hate for them to lose their progress.

Most of our learners are still using IE8 and we're publishing to SCORM 1.2.

Any thoughts?

Thanks!
Scott

Mark King

After reading down the thread, it does seem that Ben was not getting a clear response regarding his observations about the behavior between the course and the browser and the browser and the LMS. I have observed the same issue and Saba Engineers have confirmed that all of those LMSSetValues are not actual saves back to the LMS database. Those are examples of the communication between the course and the browser and until an LMSCommit is invoked, nothing is written back to the LMS.

So, if that is actually what is happening, I agree with Ben that waiting that long to pass suspend data back to the LMS puts the learner at risk that their bookmarking data will not be saved if there isn't a 'elegant' exit from the course. In the log file below you can see an LMSCommt invoked after each 10 minutes in the course. First one just after launch at 4:34:07, the next one at 4:44:07 and another at 4:54:07. I'm going to guess that Articulate is invoking the LMSCommit. What I'm wondering is whether or not this is configurable?

Yesterday I tested a scenario where I used Saba's diagnostic player which allows me to view the data flowing from the course to the browser. I launched the course as a learner, navigated forward through the content and then I pulled the network cable (no wireless is active). The course allowed me to click through a couple of additional slides (I assume they were cached?) before it freezes up. I'm assuming it's freezing because the course is attempting to download additional data from the content server?

I next X'd out of the course and in the log I could see what appeared to be a normal sequence of LMSCommit and LMSFinish commands with NO errors noted.

======================================================

This log data is for the Employee Safety Course. I have been using the Preview option in the content repository to gather this data.

In this instance, I did not resume where I left off and was taken to the beginning of the course.

4:34:06 PM LMSInitialize() returned 'true'

4:34:07 PM LMSGetValue('cmi.core.lesson_mode') returned 'normal'

4:34:07 PM LMSGetLastError() returned '0'

4:34:07 PM LMSGetValue('cmi.core.lesson_mode') returned 'normal'

4:34:07 PM LMSGetLastError() returned '0'

4:34:07 PM LMSGetValue('cmi.core.lesson_status') returned 'incomplete'

4:34:07 PM LMSGetLastError() returned '0'

4:34:07 PM LMSSetValue('cmi.core.exit', 'suspend') returned 'true'

4:34:07 PM LMSSetValue('cmi.core.lesson_status', 'incomplete') returned 'true'

4:34:07 PM LMSSetValue('cmi.core.session_time', '0000:00:00.33') returned 'true'

4:34:07 PM LMSCommit() returned 'true'

4:34:07 PM LMSGetValue('cmi.suspend_data') returned 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15|lastviewedslide=15|14#2##,7,7,6,3,7,3,7,6,6,6,6,6,7,7,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,5cf4a87d-e5a8-46a2-b55e-535bf0b0c8f4=0.0,#-1'

4:34:07 PM LMSGetLastError() returned '0'

4:34:07 PM LMSGetValue('cmi.core.lesson_status') returned 'incomplete'

4:34:07 PM LMSGetLastError() returned '0'

4:34:16 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15|lastviewedslide=1|0#2##,1,1,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:34:17 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15|lastviewedslide=1|0#2##,11,1,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:34:31 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2|lastviewedslide=2|1#2##,7,11,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:34:43 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3|lastviewedslide=3|1#2##,7,11,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:34:43 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3|lastviewedslide=3|2#2##,7,7,10,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:34:53 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3|lastviewedslide=3|3#2##,7,7,14,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:01 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4|lastviewedslide=4|3#2##,7,7,6,11,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:11 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5|lastviewedslide=5|3#2##,7,7,6,3,11,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:26 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6|lastviewedslide=6|3#2##,7,7,6,3,3,11,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:30 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7|lastviewedslide=7|3#2##,7,7,6,3,3,3,11,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:42 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8|lastviewedslide=8|7#2##,7,7,6,3,3,3,7,10,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:46 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9|lastviewedslide=9|8#2##,7,7,6,3,3,3,7,6,10,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:35:57 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10|lastviewedslide=10|9#2##,7,7,6,3,3,3,7,6,6,10,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:36:05 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11|lastviewedslide=11|10#2##,7,7,6,3,3,3,7,6,6,6,10,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:36:10 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12|lastviewedslide=12|11#2##,7,7,6,3,3,3,7,6,6,6,6,10,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0##-1') returned 'true'

4:36:24 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=,#-1') returned 'true'

4:36:26 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=-1.I,#-1') returned 'true'

4:44:07 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=-1.I,#-1') returned 'true'

4:44:07 PM LMSSetValue('cmi.core.session_time', '0000:10:00.33') returned 'true'

4:44:07 PM LMSCommit() returned 'true'

4:45:37 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=0.I.0,#-1') returned 'true'

4:52:14 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=1.I.0.1,#-1') returned 'true'

4:53:22 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=2.I.0.1.2,#-1') returned 'true'

4:53:24 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=3.I.0.1.2.3,#-1') returned 'true'

4:53:25 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=4.I.0.1.2.3.4,#-1') returned 'true'

4:53:26 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13|lastviewedslide=13|12#2##,7,7,6,3,3,3,7,6,6,6,6,6,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:53:29 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14|lastviewedslide=14|12#3##,7,7,6,3,3,3,7,6,6,6,6,6,3,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:53:44 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15|lastviewedslide=15|14#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:54:07 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15|lastviewedslide=15|14#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:54:07 PM LMSSetValue('cmi.core.session_time', '0000:20:00.32') returned 'true'

4:54:07 PM LMSCommit() returned 'true'

Articulate Engage item froze here. This is a separate issue and not related to what I'm actually testing. 

I clicked past the engage item until I got to another section with a normal slide and then I pulled the network cable and tried to navigate further. 

You can see that I was able to navigate beyond slide 15 but then on slide 31, the slide wouldn't load completely but I didn't see any errors.

I think the jump from slide 19 to 31 is because the course has branching options in it for the learner. 

Finally I exited the course. See below.

dropped network:

4:55:47 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16|lastviewedslide=16|14#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:55:53 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17|lastviewedslide=17|14#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:56:31 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17|lastviewedslide=17|17#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:56:32 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18|lastviewedslide=18|17#2##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,7,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:56:39 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19|lastviewedslide=19|17#3##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,7,3,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:56:41 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19|lastviewedslide=19|19#3##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,7,3,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

4:56:44 PM LMSSetValue('cmi.suspend_data', 'viewed=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,31|lastviewedslide=31|19#3##,7,7,6,3,3,3,7,6,6,6,6,6,3,7,3,3,7,3,7,1,1,1,1,1,1,1,1,1,1,1,11,1,1,1,1,1,1,1,1,1,1,1,1,1,1#0,0,0#c7ea9039-4bfa-4248-b27f-092dfb64f83b=5.I.0.1.2.3.4.5,#-1') returned 'true'

exited course:

4:58:19 PM LMSSetValue('cmi.core.session_time', '0000:24:12.12') returned 'true'

4:58:19 PM LMSSetValue('cmi.core.exit', 'suspend') returned 'true'

4:58:19 PM LMSCommit() returned 'true'

4:58:19 PM LMSFinish() returned 'true'

When I tried to close the diagnostic player, it wouldn't close and displayed a message "Unloading content. Please wait."

I finally just closed the browser tab.

================================================

Ashley Terwilliger

Hi Mark,

The LMSCommit message is sent from Storyline once you've reached the completion requirement although Storyline and your LMS are communicating throughout the course. Also, you are likely able to click through a few other slides as Storyline progressively loads content. As Steve mentioned above you could set it up to invoke LMS Commit on every slide using the Javascript triggers. 

There is also a method described here on how to let your users know that the course attempt has not been saved in the event of any error. 

Mark King

Thank you Ashley for the response. I have to admit that your statement that, "Storyline and your LMS are communicating throughout the course" doesn't jive with what our testing shows as well as with what the vendor is telling us.

--------------------

The SCORM Version 1.2 Standard  reads: 

Page 3-7

Description of the SCO to LMS Communication API

A key aspect of the API is that it is a communication mechanism that allows the SCO to communicate with the LMS. It is assmed that once the SCO is launched it can then "get" and "set" information with the LMS. All communication between the API Adapter and the SCO is initiated by the SCO. There is currently no supported mechanism for LMSs to initiate calls to functions implmented by a SCO.

Page 3-10

LMSSetvalue:

"This function allows the SCO to send information to the LMS. The API Adapter may be designed to immediately forward the information to the LMS, or it may be designed to forward information based on some other approach."

Page 3-11

LMSCommit

If the API Adapter is caching values received from the SCO via an LMSSetValue(), this call requires that any values not yet persisted by the LMS be persisted.

In some implementations, the API Adapter may persist set values as soon as they are received, and not cache them on the client. In such implementations, the API call is redundant and would result in no additional action from the API Adapter. This call ensures to the SCO that the data sent, via an LMSSetValue() call, will be persisted by the LMS upon completion of the LMSCommit().

------------------

I interpret the excerpts from the standard above as leaving the decision regarding the frequency of the LMSCommit() command communication up to the SCO although it seems that the API can make decisions about what to do with the interim LMSSetValue() commands. It can issue them immediately upon receipt from the SCO or it can choose cache them. Certainly the bare minimum communication is at the LMSInitialize, LMSCommit, and LMSFinish. The standard also calls out that the only way the SCO can be sure that the data sent to the LMS will be "persisted" is to issue an LMSCommit() command.

So, if Saba's API is caching the LMSSetValue commands, then it appears that Storyline has established an interim frequency communication model of once every 10 minutes (at minimum) using the LMSCommit() command.

My testing suggests that Storyline is making this decision because the diagnostic template I'm using is Saba's and, according to Saba, the commands I see in the log are those issued between the SCO and the API along with any responses that the API returns to the SCO.

If Storyline is making that decision, then in my opinion once every 10 minutes until the learner either reaches the completion requirement for the course or decides to exit the course is simply too long.

And, yes we have been successful using javascript to cause the LMSCommit() to be issued on each new slide and that will help us get around the problem, but I hope you will agree that allowing the content author (in consultation with their internal LMS administrator) the option to modify the 'default' LMSCommit() frequency using a simple parameter defined when the course is published would be much easier than working with javascript. Afterall, it seems that the content author is the person most qualified to determine whether or not any default value is too long.

So, my question remains. Other than at start, resume or exit, how frequently does Storyline (or any other version of Articulate) initiate an LMSCommit() command while a learner is 'consuming' content? Once I know the answer to that question, then I can go back to Saba to determine if they are overriding the LMSCommit() command, which, according to the standard, they shouldn't be doing.

Steve Flowers

Hey Mark -

There are two API adaptors in an LMS SCORM implementation. One is the client side adaptor and the other is on the server.


"In some implementations, the API Adapter may persist set values as soon as they are received, and not cache them on the client. In such implementations, the API call is redundant and would result in no additional action from the API Adapter. This call ensures to the SCO that the data sent, via an LMSSetValue() call, will be persisted by the LMS upon completion of the LMSCommit()."

This refers to the server / LMS side of the adaptor, not to the client side adaptor. Some LMS will persist LMSSetValue immediately as called. You'll see this in the SCORM Cloud report above. The LMS is seeing the calls and sticks them as they come in. LMSCommit() isn't required for some API implementations on the LMS side. I haven't tested, but I suspect I could pull the net connection before commit is called in many LMS products and each setValue call is being recorded. 

It appears that this is not the case with Saba. Adding a master slide level JavaScript trigger when the timeline starts that calls LMSCommit() should give you greater frequency. This is described above.

Steve Flowers

Hmm... This is a Storyline thread. Looks like the logs you posted above are for Presenter. Different setup as Presenter doesn't support triggers. I suspect you'd be able to modify the LMSAPI to add a commit after every SetValue call or setup an interval that provides a commit at least every X seconds when a SetValue call is made.

Steve Flowers

Add'l info. I just checked a sequence from a Lectora course (another authoring tool). This shows 2 commits. It's making a commit right after the module loads for some reason and another when the module is exited. So the behavior you describe above is not unusual for SCORM authoring tool outputs. Haven't tested with Captivate but I'd suspect it also behaves with a single commit on exit.

Storyline offers a more frequent commit. Presenter seems to be on a 10 minute commit timer. I don't have presenter loaded at the moment. But you might be able to trace through the API functions in the SCO (or in the template folder in your Presenter / Studio program folders) to change the frequency of the commit call.

Mark King

Hi Steve,

Thank you for your responses.  We're trying to keep Saba's support team focused on this issue so I've done the best I can to try and provoke a response even if it is that my assumptions and conclusions are wrong. 

I have a couple of questions:

1. If there are two API adapters, are both provided by the LMS vendor?

2. Can I assume that the API adapter running on the server makes the final decisions about when to write data to the database regardless of how often an LMSCommit() is called by the SCO or by the client-side API adapter?

3. You mentioned in your first reply to my post about a "SCORM Cloud report above"? I can't find it but maybe I'm misunderstanding your reply?